1 package PMG
::Config
::Base
;
9 use PVE
::JSONSchema
qw(get_standard_option);
10 use PVE
::SectionConfig
;
12 use base
qw(PVE::SectionConfig);
16 type
=> { description
=> "Section type." },
18 description
=> "Section ID.",
19 type
=> 'string', format
=> 'pve-configid',
28 sub format_section_header
{
29 my ($class, $type, $sectionId) = @_;
31 die "internal error ($type ne $sectionId)" if $type ne $sectionId;
33 return "section: $type\n";
37 sub parse_section_header
{
38 my ($class, $line) = @_;
40 if ($line =~ m/^section:\s*(\S+)\s*$/) {
42 my $errmsg = undef; # set if you want to skip whole section
43 eval { PVE
::JSONSchema
::pve_verify_configid
($section); };
45 my $config = {}; # to return additional attributes
46 return ($section, $section, $errmsg, $config);
51 package PMG
::Config
::Admin
;
56 use base
qw(PMG::Config::Base);
65 description
=> "Use advanced filters for statistic.",
70 description
=> "Send daily reports.",
75 description
=> "User Statistics Lifetime (days)",
81 description
=> "Demo mode - do not start SMTP filter.",
86 description
=> "Administrator E-Mail address.",
87 type
=> 'string', format
=> 'email',
88 default => 'admin@domain.tld',
91 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
93 pattern
=> "http://.*",
96 description
=> "Use Avast Virus Scanner (/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.",
101 description
=> "Use ClamAV Virus Scanner. This is the default virus scanner and is enabled by default.",
106 description
=> "Use Custom Check Script. The script has to take the defined arguments and can return Virus findings or a Spamscore.",
110 custom_check_path
=> {
111 description
=> "Absolute Path to the Custom Check Script",
112 type
=> 'string', pattern
=> '^/([^/\0]+\/)+[^/\0]+$',
113 default => '/usr/local/bin/pmg-custom-check',
120 advfilter
=> { optional
=> 1 },
121 avast
=> { optional
=> 1 },
122 clamav
=> { optional
=> 1 },
123 statlifetime
=> { optional
=> 1 },
124 dailyreport
=> { optional
=> 1 },
125 demo
=> { optional
=> 1 },
126 email
=> { optional
=> 1 },
127 http_proxy
=> { optional
=> 1 },
128 custom_check
=> { optional
=> 1 },
129 custom_check_path
=> { optional
=> 1 },
133 package PMG
::Config
::Spam
;
138 use base
qw(PMG::Config::Base);
147 description
=> "This option is used to specify which languages are considered OK for incoming mail.",
149 pattern
=> '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
153 description
=> "Whether to use the naive-Bayesian-style classifier.",
158 description
=> "Use the Auto-Whitelist plugin.",
163 description
=> "Whether to use Razor2, if it is available.",
167 wl_bounce_relays
=> {
168 description
=> "Whitelist legitimate bounce relays.",
171 clamav_heuristic_score
=> {
172 description
=> "Score for ClamAV heuristics (Google Safe Browsing database, PhishingScanURLs, ...).",
179 description
=> "Additional score for bounce mails.",
186 description
=> "Enable real time blacklists (RBL) checks.",
191 description
=> "Maximum size of spam messages in bytes.",
201 use_awl
=> { optional
=> 1 },
202 use_razor
=> { optional
=> 1 },
203 wl_bounce_relays
=> { optional
=> 1 },
204 languages
=> { optional
=> 1 },
205 use_bayes
=> { optional
=> 1 },
206 clamav_heuristic_score
=> { optional
=> 1 },
207 bounce_score
=> { optional
=> 1 },
208 rbl_checks
=> { optional
=> 1 },
209 maxspamsize
=> { optional
=> 1 },
213 package PMG
::Config
::SpamQuarantine
;
218 use base
qw(PMG::Config::Base);
227 description
=> "Quarantine life time (days)",
233 description
=> "Authentication mode to access the quarantine interface. Mode 'ticket' allows login using tickets sent with the daily spam report. Mode 'ldap' requires to login using an LDAP account. Finally, mode 'ldapticket' allows both ways.",
235 enum
=> [qw(ticket ldap ldapticket)],
239 description
=> "Spam report style.",
241 enum
=> [qw(none short verbose custom)],
242 default => 'verbose',
245 description
=> "Allow to view images.",
250 description
=> "Allow to view hyperlinks.",
255 description
=> "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
256 type
=> 'string', format
=> 'address',
259 description
=> "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
266 description
=> "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
268 enum
=> [qw(http https)],
272 description
=> "Text for 'From' header in daily spam report mails.",
280 mailfrom
=> { optional
=> 1 },
281 hostname
=> { optional
=> 1 },
282 lifetime
=> { optional
=> 1 },
283 authmode
=> { optional
=> 1 },
284 reportstyle
=> { optional
=> 1 },
285 viewimages
=> { optional
=> 1 },
286 allowhrefs
=> { optional
=> 1 },
287 port
=> { optional
=> 1 },
288 protocol
=> { optional
=> 1 },
292 package PMG
::Config
::VirusQuarantine
;
297 use base
qw(PMG::Config::Base);
309 lifetime
=> { optional
=> 1 },
310 viewimages
=> { optional
=> 1 },
311 allowhrefs
=> { optional
=> 1 },
315 package PMG
::Config
::ClamAV
;
320 use base
qw(PMG::Config::Base);
329 description
=> "ClamAV database mirror server.",
331 default => 'database.clamav.net',
333 archiveblockencrypted
=> {
334 description
=> "Whether to block encrypted archives. Mark encrypted archives as viruses.",
339 description
=> "Nested archives are scanned recursively, e.g. if a ZIP archive contains a TAR file, all files within it will also be scanned. This options specifies how deeply the process should be continued. Warning: setting this limit too high may result in severe damage to the system.",
345 description
=> "Number of files to be scanned within an archive, a document, or any other kind of container. Warning: disabling this limit or setting it too high may result in severe damage to the system.",
351 description
=> "Files larger than this limit won't be scanned.",
357 description
=> "Sets the maximum amount of data to be scanned for each input file.",
360 default => 100000000,
363 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
369 description
=> "Enables support for Google Safe Browsing.",
378 archiveblockencrypted
=> { optional
=> 1 },
379 archivemaxrec
=> { optional
=> 1 },
380 archivemaxfiles
=> { optional
=> 1 },
381 archivemaxsize
=> { optional
=> 1 },
382 maxscansize
=> { optional
=> 1 },
383 dbmirror
=> { optional
=> 1 },
384 maxcccount
=> { optional
=> 1 },
385 safebrowsing
=> { optional
=> 1 },
389 package PMG
::Config
::Mail
;
394 use PVE
::ProcFSTools
;
396 use base
qw(PMG::Config::Base);
403 sub physical_memory
{
405 return $physicalmem if $physicalmem;
407 my $info = PVE
::ProcFSTools
::read_meminfo
();
408 my $total = int($info->{memtotal
} / (1024*1024));
413 sub get_max_filters
{
414 # estimate optimal number of filter servers
418 my $memory = physical_memory
();
419 my $add_servers = int(($memory - 512)/$servermem);
420 $max_servers += $add_servers if $add_servers > 0;
421 $max_servers = 40 if $max_servers > 40;
423 return $max_servers - 2;
427 # estimate optimal number of smtpd daemons
429 my $max_servers = 25;
431 my $memory = physical_memory
();
432 my $add_servers = int(($memory - 512)/$servermem);
433 $max_servers += $add_servers if $add_servers > 0;
434 $max_servers = 100 if $max_servers > 100;
439 # estimate optimal number of proxpolicy servers
441 my $memory = physical_memory
();
442 $max_servers = 5 if $memory >= 500;
449 description
=> "SMTP port number for outgoing mail (trusted).",
456 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
463 description
=> "The default mail delivery transport (incoming mails).",
464 type
=> 'string', format
=> 'address',
467 description
=> "SMTP port number for relay host.",
474 description
=> "Disable MX lookups for default relay.",
479 description
=> "When set, all outgoing mails are deliverd to the specified smarthost.",
480 type
=> 'string', format
=> 'address',
483 description
=> "SMTP port number for smarthost.",
490 description
=> "ESMTP banner.",
493 default => 'ESMTP Proxmox',
496 description
=> "Maximum number of pmg-smtp-filter processes.",
500 default => get_max_filters
(),
503 description
=> "Maximum number of pmgpolicy processes.",
507 default => get_max_policy
(),
510 description
=> "Maximum number of SMTP daemon processes (in).",
514 default => get_max_smtpd
(),
517 description
=> "Maximum number of SMTP daemon processes (out).",
521 default => get_max_smtpd
(),
523 conn_count_limit
=> {
524 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
530 description
=> "The maximal number of connection attempts any client is allowed to make to this service per minute. To disable this feature, specify a limit of 0.",
535 message_rate_limit
=> {
536 description
=> "The maximal number of message delivery requests that any client is allowed to make to this service per minute.To disable this feature, specify a limit of 0.",
542 description
=> "Hide received header in outgoing mails.",
547 description
=> "Maximum email size. Larger mails are rejected.",
550 default => 1024*1024*10,
553 description
=> "SMTP delay warning time (in hours).",
559 description
=> "Enable TLS.",
564 description
=> "Enable TLS Logging.",
569 description
=> "Add TLS received header.",
574 description
=> "Use Sender Policy Framework.",
579 description
=> "Use Greylisting.",
584 description
=> "Use SMTP HELO tests.",
589 description
=> "Reject unknown clients.",
593 rejectunknownsender
=> {
594 description
=> "Reject unknown senders.",
599 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
601 enum
=> ['450', '550'],
604 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
605 type
=> 'string', format
=> 'dnsbl-entry-list',
608 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
618 int_port
=> { optional
=> 1 },
619 ext_port
=> { optional
=> 1 },
620 smarthost
=> { optional
=> 1 },
621 smarthostport
=> { optional
=> 1 },
622 relay
=> { optional
=> 1 },
623 relayport
=> { optional
=> 1 },
624 relaynomx
=> { optional
=> 1 },
625 dwarning
=> { optional
=> 1 },
626 max_smtpd_in
=> { optional
=> 1 },
627 max_smtpd_out
=> { optional
=> 1 },
628 greylist
=> { optional
=> 1 },
629 helotests
=> { optional
=> 1 },
630 tls
=> { optional
=> 1 },
631 tlslog
=> { optional
=> 1 },
632 tlsheader
=> { optional
=> 1 },
633 spf
=> { optional
=> 1 },
634 maxsize
=> { optional
=> 1 },
635 banner
=> { optional
=> 1 },
636 max_filters
=> { optional
=> 1 },
637 max_policy
=> { optional
=> 1 },
638 hide_received
=> { optional
=> 1 },
639 rejectunknown
=> { optional
=> 1 },
640 rejectunknownsender
=> { optional
=> 1 },
641 conn_count_limit
=> { optional
=> 1 },
642 conn_rate_limit
=> { optional
=> 1 },
643 message_rate_limit
=> { optional
=> 1 },
644 verifyreceivers
=> { optional
=> 1 },
645 dnsbl_sites
=> { optional
=> 1 },
646 dnsbl_threshold
=> { optional
=> 1 },
659 use PVE
::Tools
qw($IPV4RE $IPV6RE);
665 PMG
::Config
::Admin-
>register();
666 PMG
::Config
::Mail-
>register();
667 PMG
::Config
::SpamQuarantine-
>register();
668 PMG
::Config
::VirusQuarantine-
>register();
669 PMG
::Config
::Spam-
>register();
670 PMG
::Config
::ClamAV-
>register();
672 # initialize all plugins
673 PMG
::Config
::Base-
>init();
675 PVE
::JSONSchema
::register_format
(
676 'transport-domain', \
&pmg_verify_transport_domain
);
678 sub pmg_verify_transport_domain
{
679 my ($name, $noerr) = @_;
681 # like dns-name, but can contain leading dot
682 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
684 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
685 return undef if $noerr;
686 die "value does not look like a valid transport domain\n";
691 PVE
::JSONSchema
::register_format
(
692 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
694 sub pmg_verify_transport_domain_or_email
{
695 my ($name, $noerr) = @_;
697 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
700 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
704 # like dns-name, but can contain leading dot
705 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
706 return undef if $noerr;
707 die "value does not look like a valid transport domain or email address\n";
712 PVE
::JSONSchema
::register_format
(
713 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
715 sub pmg_verify_dnsbl_entry
{
716 my ($name, $noerr) = @_;
718 # like dns-name, but can contain trailing weight: 'domain*<WEIGHT>'
719 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
721 if ($name !~ /^(${namere}\.)*${namere}(\*\-?\d+)?$/) {
722 return undef if $noerr;
723 die "value '$name' does not look like a valid dnsbl entry\n";
731 my $class = ref($type) || $type;
733 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
735 return bless $cfg, $class;
741 PVE
::INotify
::write_file
("pmg.conf", $self);
744 my $lockfile = "/var/lock/pmgconfig.lck";
747 my ($code, $errmsg) = @_;
749 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
751 $errmsg ?
die "$errmsg: $err" : die $err;
757 my ($self, $section, $key, $value) = @_;
759 my $pdata = PMG
::Config
::Base-
>private();
761 my $plugin = $pdata->{plugins
}->{$section};
762 die "no such section '$section'" if !$plugin;
764 if (defined($value)) {
765 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
766 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
767 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
769 if (defined($self->{ids
}->{$section})) {
770 delete $self->{ids
}->{$section}->{$key};
777 # get section value or default
779 my ($self, $section, $key, $nodefault) = @_;
781 my $pdata = PMG
::Config
::Base-
>private();
782 my $pdesc = $pdata->{propertyList
}->{$key};
783 die "no such property '$section/$key'\n"
784 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
785 defined($pdata->{options
}->{$section}->{$key}));
787 if (defined($self->{ids
}->{$section}) &&
788 defined(my $value = $self->{ids
}->{$section}->{$key})) {
792 return undef if $nodefault;
794 return $pdesc->{default};
797 # get a whole section with default value
799 my ($self, $section) = @_;
801 my $pdata = PMG
::Config
::Base-
>private();
802 return undef if !defined($pdata->{options
}->{$section});
806 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
808 my $pdesc = $pdata->{propertyList
}->{$key};
810 if (defined($self->{ids
}->{$section}) &&
811 defined(my $value = $self->{ids
}->{$section}->{$key})) {
812 $res->{$key} = $value;
815 $res->{$key} = $pdesc->{default};
821 # get a whole config with default values
825 my $pdata = PMG
::Config
::Base-
>private();
829 foreach my $type (keys %{$pdata->{plugins
}}) {
830 my $plugin = $pdata->{plugins
}->{$type};
831 $res->{$type} = $self->get_section($type);
838 my ($filename, $fh) = @_;
840 local $/ = undef; # slurp mode
842 my $raw = <$fh> if defined($fh);
844 return PMG
::Config
::Base-
>parse_config($filename, $raw);
848 my ($filename, $fh, $cfg) = @_;
850 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
852 PVE
::Tools
::safe_print
($filename, $fh, $raw);
855 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
858 undef, always_call_parser
=> 1);
860 # parsers/writers for other files
862 my $domainsfilename = "/etc/pmg/domains";
864 sub postmap_pmg_domains
{
865 PMG
::Utils
::run_postmap
($domainsfilename);
868 sub read_pmg_domains
{
869 my ($filename, $fh) = @_;
875 while (defined(my $line = <$fh>)) {
877 next if $line =~ m/^\s*$/;
878 if ($line =~ m/^#(.*)\s*$/) {
882 if ($line =~ m/^(\S+)\s.*$/) {
884 $domains->{$domain} = {
885 domain
=> $domain, comment
=> $comment };
888 warn "parse error in '$filename': $line\n";
897 sub write_pmg_domains
{
898 my ($filename, $fh, $domains) = @_;
900 foreach my $domain (sort keys %$domains) {
901 my $comment = $domains->{$domain}->{comment
};
902 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
903 if defined($comment) && $comment !~ m/^\s*$/;
905 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
909 PVE
::INotify
::register_file
('domains', $domainsfilename,
912 undef, always_call_parser
=> 1);
914 my $mynetworks_filename = "/etc/pmg/mynetworks";
916 sub read_pmg_mynetworks
{
917 my ($filename, $fh) = @_;
923 while (defined(my $line = <$fh>)) {
925 next if $line =~ m/^\s*$/;
926 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
927 my ($network, $prefix_size, $comment) = ($1, $2, $3);
928 my $cidr = "$network/${prefix_size}";
929 $mynetworks->{$cidr} = {
931 network_address
=> $network,
932 prefix_size
=> $prefix_size,
933 comment
=> $comment // '',
936 warn "parse error in '$filename': $line\n";
944 sub write_pmg_mynetworks
{
945 my ($filename, $fh, $mynetworks) = @_;
947 foreach my $cidr (sort keys %$mynetworks) {
948 my $data = $mynetworks->{$cidr};
949 my $comment = $data->{comment
} // '*';
950 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
954 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
955 \
&read_pmg_mynetworks
,
956 \
&write_pmg_mynetworks
,
957 undef, always_call_parser
=> 1);
959 PVE
::JSONSchema
::register_format
(
960 'tls-policy', \
&pmg_verify_tls_policy
);
962 # TODO: extend to parse attributes of the policy
963 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
964 sub pmg_verify_tls_policy
{
965 my ($policy, $noerr) = @_;
967 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
968 return undef if $noerr;
969 die "value '$policy' does not look like a valid tls policy\n";
974 PVE
::JSONSchema
::register_format
(
975 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
977 sub pmg_verify_tls_policy_strict
{
978 my ($policy, $noerr) = @_;
980 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
981 return undef if $noerr;
982 die "value '$policy' does not look like a valid tls policy\n";
987 sub read_tls_policy
{
988 my ($filename, $fh) = @_;
990 return {} if !defined($fh);
994 while (defined(my $line = <$fh>)) {
996 next if $line =~ m/^\s*$/;
997 next if $line =~ m/^#(.*)\s*$/;
999 my $parse_error = sub {
1001 die "parse error in '$filename': $line - $err";
1004 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1005 my ($domain, $policy) = ($1, $2);
1008 pmg_verify_transport_domain
($domain);
1009 pmg_verify_tls_policy
($policy);
1012 $parse_error->($err);
1016 $tls_policy->{$domain} = {
1021 $parse_error->('wrong format');
1028 sub write_tls_policy
{
1029 my ($filename, $fh, $tls_policy) = @_;
1031 return if !$tls_policy;
1033 foreach my $domain (sort keys %$tls_policy) {
1034 my $entry = $tls_policy->{$domain};
1035 PVE
::Tools
::safe_print
(
1036 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1040 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1041 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1044 undef, always_call_parser
=> 1);
1046 sub postmap_tls_policy
{
1047 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1050 my $transport_map_filename = "/etc/pmg/transport";
1052 sub postmap_pmg_transport
{
1053 PMG
::Utils
::run_postmap
($transport_map_filename);
1056 sub read_transport_map
{
1057 my ($filename, $fh) = @_;
1059 return [] if !defined($fh);
1065 while (defined(my $line = <$fh>)) {
1067 next if $line =~ m/^\s*$/;
1068 if ($line =~ m/^#(.*)\s*$/) {
1073 my $parse_error = sub {
1075 warn "parse error in '$filename': $line - $err";
1079 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1080 my ($domain, $host, $port) = ($1, $2, $3);
1082 eval { pmg_verify_transport_domain_or_email
($domain); };
1084 $parse_error->($err);
1088 if ($host =~ m/^\[(.*)\]$/) {
1093 eval { PVE
::JSONSchema
::pve_verify_address
($host); };
1095 $parse_error->($err);
1104 comment
=> $comment,
1106 $res->{$domain} = $data;
1109 $parse_error->('wrong format');
1116 sub write_transport_map
{
1117 my ($filename, $fh, $tmap) = @_;
1121 foreach my $domain (sort keys %$tmap) {
1122 my $data = $tmap->{$domain};
1124 my $comment = $data->{comment
};
1125 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1126 if defined($comment) && $comment !~ m/^\s*$/;
1128 my $use_mx = $data->{use_mx
};
1129 $use_mx = 0 if $data->{host
} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1132 PVE
::Tools
::safe_print
(
1133 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1135 PVE
::Tools
::safe_print
(
1136 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1141 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1142 \
&read_transport_map
,
1143 \
&write_transport_map
,
1144 undef, always_call_parser
=> 1);
1146 # config file generation using templates
1148 sub get_template_vars
{
1151 my $vars = { pmg
=> $self->get_config() };
1153 my $nodename = PVE
::INotify
::nodename
();
1154 my $int_ip = PMG
::Cluster
::remote_node_ip
($nodename);
1155 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1157 my $transportnets = [];
1159 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1160 foreach my $domain (sort keys %$tmap) {
1161 my $data = $tmap->{$domain};
1162 my $host = $data->{host
};
1163 if ($host =~ m/^$IPV4RE$/) {
1164 push @$transportnets, "$host/32";
1165 } elsif ($host =~ m/^$IPV6RE$/) {
1166 push @$transportnets, "[$host]/128";
1171 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1173 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1175 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1176 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1177 push @$mynetworks, "[$1]/$2";
1179 push @$mynetworks, $int_net_cidr;
1182 if ($int_ip =~ m/^$IPV6RE$/) {
1183 push @$mynetworks, "[$int_ip]/128";
1185 push @$mynetworks, "$int_ip/32";
1189 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1190 foreach my $cidr (keys %$netlist) {
1191 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1192 push @$mynetworks, "[$1]/$2";
1194 push @$mynetworks, $cidr;
1198 push @$mynetworks, @$transportnets;
1200 # add default relay to mynetworks
1201 if (my $relay = $self->get('mail', 'relay')) {
1202 if ($relay =~ m/^$IPV4RE$/) {
1203 push @$mynetworks, "$relay/32";
1204 } elsif ($relay =~ m/^$IPV6RE$/) {
1205 push @$mynetworks, "[$relay]/128";
1207 # DNS name - do nothing ?
1211 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1213 # normalize dnsbl_sites
1214 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1215 if (scalar(@dnsbl_sites)) {
1216 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1219 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1222 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1223 $self->get('mail', 'spf');
1224 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1226 if ($int_ip =~ m/^$IPV6RE$/) {
1227 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1229 $vars->{postfix
}->{int_ip
} = $int_ip;
1232 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1233 $vars->{dns
}->{hostname
} = $nodename;
1235 my $domain = $resolv->{search
} // 'localdomain';
1236 $vars->{dns
}->{domain
} = $domain;
1238 my $wlbr = "$nodename.$domain";
1239 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1242 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1244 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1246 my $uri = URI-
>new($proxy);
1247 my $host = $uri->host;
1248 my $port = $uri->port // 8080;
1250 my $data = { host
=> $host, port
=> $port };
1251 if (my $ui = $uri->userinfo) {
1252 my ($username, $pw) = split(/:/, $ui, 2);
1253 $data->{username
} = $username;
1254 $data->{password
} = $pw if defined($pw);
1256 $vars->{proxy
} = $data;
1259 warn "parse http_proxy failed - $@" if $@;
1265 # use one global TT cache
1266 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1268 my $template_toolkit;
1270 sub get_template_toolkit
{
1272 return $template_toolkit if $template_toolkit;
1274 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1276 return $template_toolkit;
1279 # rewrite file from template
1280 # return true if file has changed
1281 sub rewrite_config_file
{
1282 my ($self, $tmplname, $dstfn) = @_;
1284 my $demo = $self->get('admin', 'demo');
1287 my $demosrc = "$tmplname.demo";
1288 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1291 my ($perm, $uid, $gid);
1293 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1294 # needed if file contains a HTTPProxyPasswort
1296 $uid = getpwnam('clamav');
1297 $gid = getgrnam('adm');
1301 my $tt = get_template_toolkit
();
1303 my $vars = $self->get_template_vars();
1307 $tt->process($tmplname, $vars, \
$output) ||
1308 die $tt->error() . "\n";
1310 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1312 return 0 if defined($old) && ($old eq $output); # no change
1314 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1316 if (defined($uid) && defined($gid)) {
1317 chown($uid, $gid, $dstfn);
1323 # rewrite spam configuration
1324 sub rewrite_config_spam
{
1327 my $use_awl = $self->get('spam', 'use_awl');
1328 my $use_bayes = $self->get('spam', 'use_bayes');
1329 my $use_razor = $self->get('spam', 'use_razor');
1333 # delete AW and bayes databases if those features are disabled
1335 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1339 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1340 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1341 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1344 # make sure we have a custom.cf file (else cluster sync fails)
1345 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1347 $changes = 1 if $self->rewrite_config_file(
1348 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1350 $changes = 1 if $self->rewrite_config_file(
1351 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1353 $changes = 1 if $self->rewrite_config_file(
1354 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1356 $changes = 1 if $self->rewrite_config_file(
1357 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1360 mkdir "/root/.razor";
1362 $changes = 1 if $self->rewrite_config_file(
1363 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1365 if (! -e
'/root/.razor/identity') {
1368 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1369 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1372 syslog
('info', "registering razor failed: $err") if $err;
1379 # rewrite ClamAV configuration
1380 sub rewrite_config_clam
{
1383 return $self->rewrite_config_file(
1384 'clamd.conf.in', '/etc/clamav/clamd.conf');
1387 sub rewrite_config_freshclam
{
1390 return $self->rewrite_config_file(
1391 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1394 sub rewrite_config_postgres
{
1397 my $pgconfdir = "/etc/postgresql/9.6/main";
1401 $changes = 1 if $self->rewrite_config_file(
1402 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1404 $changes = 1 if $self->rewrite_config_file(
1405 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1410 # rewrite /root/.forward
1411 sub rewrite_dot_forward
{
1414 my $dstfn = '/root/.forward';
1416 my $email = $self->get('admin', 'email');
1419 if ($email && $email =~ m/\s*(\S+)\s*/) {
1422 # empty .forward does not forward mails (see man local)
1425 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1427 return 0 if defined($old) && ($old eq $output); # no change
1429 PVE
::Tools
::file_set_contents
($dstfn, $output);
1434 my $write_smtp_whitelist = sub {
1435 my ($filename, $data, $action) = @_;
1437 $action = 'OK' if !$action;
1439 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1443 foreach my $k (sort keys %$data) {
1444 $new .= "$k $action\n";
1447 return 0 if defined($old) && ($old eq $new); # no change
1449 PVE
::Tools
::file_set_contents
($filename, $new);
1451 PMG
::Utils
::run_postmap
($filename);
1456 sub rewrite_postfix_whitelist
{
1457 my ($rulecache) = @_;
1459 # see man page for regexp_table for postfix regex table format
1461 # we use a hash to avoid duplicate entries in regex tables
1464 my $clientlist = {};
1466 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1467 my $oclass = ref($obj);
1468 if ($oclass eq 'PMG::RuleDB::Receiver') {
1469 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1470 $tolist->{"/^$addr\$/"} = 1;
1471 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1472 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1473 $tolist->{"/^.+\@$addr\$/"} = 1;
1474 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1475 my $addr = $obj->{address
};
1477 $tolist->{"/^$addr\$/"} = 1;
1481 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1482 my $oclass = ref($obj);
1483 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1484 if ($oclass eq 'PMG::RuleDB::EMail') {
1485 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1486 $fromlist->{"/^$addr\$/"} = 1;
1487 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1488 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1489 $fromlist->{"/^.+\@$addr\$/"} = 1;
1490 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1491 my $addr = $obj->{address
};
1493 $fromlist->{"/^$addr\$/"} = 1;
1494 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1495 $clientlist->{$obj->{address
}} = 1;
1496 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1497 $clientlist->{$obj->{address
}} = 1;
1501 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1502 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1503 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1504 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1507 # rewrite /etc/postfix/*
1508 sub rewrite_config_postfix
{
1509 my ($self, $rulecache) = @_;
1511 # make sure we have required files (else postfix start fails)
1512 IO
::File-
>new($transport_map_filename, 'a', 0644);
1516 if ($self->get('mail', 'tls')) {
1518 PMG
::Utils
::gen_proxmox_tls_cert
();
1520 syslog
('info', "generating certificate failed: $@") if $@;
1523 $changes = 1 if $self->rewrite_config_file(
1524 'main.cf.in', '/etc/postfix/main.cf');
1526 $changes = 1 if $self->rewrite_config_file(
1527 'master.cf.in', '/etc/postfix/master.cf');
1529 # make sure we have required files (else postfix start fails)
1530 # Note: postmap need a valid /etc/postfix/main.cf configuration
1531 postmap_pmg_domains
();
1532 postmap_pmg_transport
();
1533 postmap_tls_policy
();
1535 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1537 # make sure aliases.db is up to date
1538 system('/usr/bin/newaliases');
1543 sub rewrite_config
{
1544 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1546 $force_restart = {} if ! $force_restart;
1548 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1549 $force_restart->{postfix
}) {
1550 PMG
::Utils
::service_cmd
('postfix', 'restart');
1553 if ($self->rewrite_dot_forward() && $restart_services) {
1554 # no need to restart anything
1557 if ($self->rewrite_config_postgres() && $restart_services) {
1558 # do nothing (too many side effects)?
1559 # does not happen anyways, because config does not change.
1562 if (($self->rewrite_config_spam() && $restart_services) ||
1563 $force_restart->{spam
}) {
1564 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1567 if (($self->rewrite_config_clam() && $restart_services) ||
1568 $force_restart->{clam
}) {
1569 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1572 if (($self->rewrite_config_freshclam() && $restart_services) ||
1573 $force_restart->{freshclam
}) {
1574 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');