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
=> "Secion 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.",
110 advfilter
=> { optional
=> 1 },
111 avast
=> { optional
=> 1 },
112 clamav
=> { optional
=> 1 },
113 statlifetime
=> { optional
=> 1 },
114 dailyreport
=> { optional
=> 1 },
115 demo
=> { optional
=> 1 },
116 email
=> { optional
=> 1 },
117 http_proxy
=> { optional
=> 1 },
121 package PMG
::Config
::Spam
;
126 use base
qw(PMG::Config::Base);
135 description
=> "This option is used to specify which languages are considered OK for incoming mail.",
137 pattern
=> '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
141 description
=> "Whether to use the naive-Bayesian-style classifier.",
146 description
=> "Use the Auto-Whitelist plugin.",
151 description
=> "Whether to use Razor2, if it is available.",
155 wl_bounce_relays
=> {
156 description
=> "Whitelist legitimate bounce relays.",
159 clamav_heuristic_score
=> {
160 description
=> "Score for ClamaAV heuristics (Google Safe Browsing database, PhishingScanURLs, ...).",
167 description
=> "Additional score for bounce mails.",
174 description
=> "Enable real time blacklists (RBL) checks.",
179 description
=> "Maximum size of spam messages in bytes.",
189 use_awl
=> { optional
=> 1 },
190 use_razor
=> { optional
=> 1 },
191 wl_bounce_relays
=> { optional
=> 1 },
192 languages
=> { optional
=> 1 },
193 use_bayes
=> { optional
=> 1 },
194 clamav_heuristic_score
=> { optional
=> 1 },
195 bounce_score
=> { optional
=> 1 },
196 rbl_checks
=> { optional
=> 1 },
197 maxspamsize
=> { optional
=> 1 },
201 package PMG
::Config
::SpamQuarantine
;
206 use base
qw(PMG::Config::Base);
215 description
=> "Quarantine life time (days)",
221 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.",
223 enum
=> [qw(ticket ldap ldapticket)],
227 description
=> "Spam report style.",
229 enum
=> [qw(none short verbose custom)],
230 default => 'verbose',
233 description
=> "Allow to view images.",
238 description
=> "Allow to view hyperlinks.",
243 description
=> "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
244 type
=> 'string', format
=> 'address',
247 description
=> "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
254 description
=> "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
256 enum
=> [qw(http https)],
260 description
=> "Text for 'From' header in daily spam report mails.",
268 mailfrom
=> { optional
=> 1 },
269 hostname
=> { optional
=> 1 },
270 lifetime
=> { optional
=> 1 },
271 authmode
=> { optional
=> 1 },
272 reportstyle
=> { optional
=> 1 },
273 viewimages
=> { optional
=> 1 },
274 allowhrefs
=> { optional
=> 1 },
275 port
=> { optional
=> 1 },
276 protocol
=> { optional
=> 1 },
280 package PMG
::Config
::VirusQuarantine
;
285 use base
qw(PMG::Config::Base);
297 lifetime
=> { optional
=> 1 },
298 viewimages
=> { optional
=> 1 },
299 allowhrefs
=> { optional
=> 1 },
303 package PMG
::Config
::ClamAV
;
308 use base
qw(PMG::Config::Base);
317 description
=> "ClamAV database mirror server.",
319 default => 'database.clamav.net',
321 archiveblockencrypted
=> {
322 description
=> "Wether to block encrypted archives. Mark encrypted archives as viruses.",
327 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.",
333 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.",
339 description
=> "Files larger than this limit won't be scanned.",
345 description
=> "Sets the maximum amount of data to be scanned for each input file.",
348 default => 100000000,
351 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
357 description
=> "Enables support for Google Safe Browsing.",
366 archiveblockencrypted
=> { optional
=> 1 },
367 archivemaxrec
=> { optional
=> 1 },
368 archivemaxfiles
=> { optional
=> 1 },
369 archivemaxsize
=> { optional
=> 1 },
370 maxscansize
=> { optional
=> 1 },
371 dbmirror
=> { optional
=> 1 },
372 maxcccount
=> { optional
=> 1 },
373 safebrowsing
=> { optional
=> 1 },
377 package PMG
::Config
::Mail
;
382 use PVE
::ProcFSTools
;
384 use base
qw(PMG::Config::Base);
391 sub physical_memory
{
393 return $physicalmem if $physicalmem;
395 my $info = PVE
::ProcFSTools
::read_meminfo
();
396 my $total = int($info->{memtotal
} / (1024*1024));
401 sub get_max_filters
{
402 # estimate optimal number of filter servers
406 my $memory = physical_memory
();
407 my $add_servers = int(($memory - 512)/$servermem);
408 $max_servers += $add_servers if $add_servers > 0;
409 $max_servers = 40 if $max_servers > 40;
411 return $max_servers - 2;
415 # estimate optimal number of smtpd daemons
417 my $max_servers = 25;
419 my $memory = physical_memory
();
420 my $add_servers = int(($memory - 512)/$servermem);
421 $max_servers += $add_servers if $add_servers > 0;
422 $max_servers = 100 if $max_servers > 100;
427 # estimate optimal number of proxpolicy servers
429 my $memory = physical_memory
();
430 $max_servers = 5 if $memory >= 500;
437 description
=> "SMTP port number for outgoing mail (trusted).",
444 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
451 description
=> "The default mail delivery transport (incoming mails).",
452 type
=> 'string', format
=> 'address',
455 description
=> "SMTP port number for relay host.",
462 description
=> "Disable MX lookups for default relay.",
467 description
=> "When set, all outgoing mails are deliverd to the specified smarthost.",
468 type
=> 'string', format
=> 'address',
471 description
=> "SMTP port number for smarthost.",
478 description
=> "ESMTP banner.",
481 default => 'ESMTP Proxmox',
484 description
=> "Maximum number of pmg-smtp-filter processes.",
488 default => get_max_filters
(),
491 description
=> "Maximum number of pmgpolicy processes.",
495 default => get_max_policy
(),
498 description
=> "Maximum number of SMTP daemon processes (in).",
502 default => get_max_smtpd
(),
505 description
=> "Maximum number of SMTP daemon processes (out).",
509 default => get_max_smtpd
(),
511 conn_count_limit
=> {
512 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
518 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.",
523 message_rate_limit
=> {
524 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.",
530 description
=> "Hide received header in outgoing mails.",
535 description
=> "Maximum email size. Larger mails are rejected.",
538 default => 1024*1024*10,
541 description
=> "SMTP delay warning time (in hours).",
547 description
=> "Enable TLS.",
552 description
=> "Enable TLS Logging.",
557 description
=> "Add TLS received header.",
562 description
=> "Use Sender Policy Framework.",
567 description
=> "Use Greylisting.",
572 description
=> "Use SMTP HELO tests.",
577 description
=> "Reject unknown clients.",
581 rejectunknownsender
=> {
582 description
=> "Reject unknown senders.",
587 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
589 enum
=> ['450', '550'],
592 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
593 type
=> 'string', format
=> 'dnsbl-entry-list',
596 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
606 int_port
=> { optional
=> 1 },
607 ext_port
=> { optional
=> 1 },
608 smarthost
=> { optional
=> 1 },
609 smarthostport
=> { optional
=> 1 },
610 relay
=> { optional
=> 1 },
611 relayport
=> { optional
=> 1 },
612 relaynomx
=> { optional
=> 1 },
613 dwarning
=> { optional
=> 1 },
614 max_smtpd_in
=> { optional
=> 1 },
615 max_smtpd_out
=> { optional
=> 1 },
616 greylist
=> { optional
=> 1 },
617 helotests
=> { optional
=> 1 },
618 tls
=> { optional
=> 1 },
619 tlslog
=> { optional
=> 1 },
620 tlsheader
=> { optional
=> 1 },
621 spf
=> { optional
=> 1 },
622 maxsize
=> { optional
=> 1 },
623 banner
=> { optional
=> 1 },
624 max_filters
=> { optional
=> 1 },
625 max_policy
=> { optional
=> 1 },
626 hide_received
=> { optional
=> 1 },
627 rejectunknown
=> { optional
=> 1 },
628 rejectunknownsender
=> { optional
=> 1 },
629 conn_count_limit
=> { optional
=> 1 },
630 conn_rate_limit
=> { optional
=> 1 },
631 message_rate_limit
=> { optional
=> 1 },
632 verifyreceivers
=> { optional
=> 1 },
633 dnsbl_sites
=> { optional
=> 1 },
634 dnsbl_threshold
=> { optional
=> 1 },
647 use PVE
::Tools
qw($IPV4RE $IPV6RE);
653 PMG
::Config
::Admin-
>register();
654 PMG
::Config
::Mail-
>register();
655 PMG
::Config
::SpamQuarantine-
>register();
656 PMG
::Config
::VirusQuarantine-
>register();
657 PMG
::Config
::Spam-
>register();
658 PMG
::Config
::ClamAV-
>register();
660 # initialize all plugins
661 PMG
::Config
::Base-
>init();
663 PVE
::JSONSchema
::register_format
(
664 'transport-domain', \
&pmg_verify_transport_domain
);
666 sub pmg_verify_transport_domain
{
667 my ($name, $noerr) = @_;
669 # like dns-name, but can contain leading dot
670 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
672 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
673 return undef if $noerr;
674 die "value does not look like a valid transport domain\n";
679 PVE
::JSONSchema
::register_format
(
680 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
682 sub pmg_verify_transport_domain_or_email
{
683 my ($name, $noerr) = @_;
685 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
688 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
692 # like dns-name, but can contain leading dot
693 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
694 return undef if $noerr;
695 die "value does not look like a valid transport domain or email address\n";
700 PVE
::JSONSchema
::register_format
(
701 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
703 sub pmg_verify_dnsbl_entry
{
704 my ($name, $noerr) = @_;
706 # like dns-name, but can contain trailing weight: 'domain*<WEIGHT>'
707 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
709 if ($name !~ /^(${namere}\.)*${namere}(\*\-?\d+)?$/) {
710 return undef if $noerr;
711 die "value '$name' does not look like a valid dnsbl entry\n";
719 my $class = ref($type) || $type;
721 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
723 return bless $cfg, $class;
729 PVE
::INotify
::write_file
("pmg.conf", $self);
732 my $lockfile = "/var/lock/pmgconfig.lck";
735 my ($code, $errmsg) = @_;
737 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
739 $errmsg ?
die "$errmsg: $err" : die $err;
745 my ($self, $section, $key, $value) = @_;
747 my $pdata = PMG
::Config
::Base-
>private();
749 my $plugin = $pdata->{plugins
}->{$section};
750 die "no such section '$section'" if !$plugin;
752 if (defined($value)) {
753 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
754 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
755 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
757 if (defined($self->{ids
}->{$section})) {
758 delete $self->{ids
}->{$section}->{$key};
765 # get section value or default
767 my ($self, $section, $key, $nodefault) = @_;
769 my $pdata = PMG
::Config
::Base-
>private();
770 my $pdesc = $pdata->{propertyList
}->{$key};
771 die "no such property '$section/$key'\n"
772 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
773 defined($pdata->{options
}->{$section}->{$key}));
775 if (defined($self->{ids
}->{$section}) &&
776 defined(my $value = $self->{ids
}->{$section}->{$key})) {
780 return undef if $nodefault;
782 return $pdesc->{default};
785 # get a whole section with default value
787 my ($self, $section) = @_;
789 my $pdata = PMG
::Config
::Base-
>private();
790 return undef if !defined($pdata->{options
}->{$section});
794 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
796 my $pdesc = $pdata->{propertyList
}->{$key};
798 if (defined($self->{ids
}->{$section}) &&
799 defined(my $value = $self->{ids
}->{$section}->{$key})) {
800 $res->{$key} = $value;
803 $res->{$key} = $pdesc->{default};
809 # get a whole config with default values
813 my $pdata = PMG
::Config
::Base-
>private();
817 foreach my $type (keys %{$pdata->{plugins
}}) {
818 my $plugin = $pdata->{plugins
}->{$type};
819 $res->{$type} = $self->get_section($type);
826 my ($filename, $fh) = @_;
828 local $/ = undef; # slurp mode
830 my $raw = <$fh> if defined($fh);
832 return PMG
::Config
::Base-
>parse_config($filename, $raw);
836 my ($filename, $fh, $cfg) = @_;
838 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
840 PVE
::Tools
::safe_print
($filename, $fh, $raw);
843 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
846 undef, always_call_parser
=> 1);
848 # parsers/writers for other files
850 my $domainsfilename = "/etc/pmg/domains";
852 sub postmap_pmg_domains
{
853 PMG
::Utils
::run_postmap
($domainsfilename);
856 sub read_pmg_domains
{
857 my ($filename, $fh) = @_;
863 while (defined(my $line = <$fh>)) {
865 next if $line =~ m/^\s*$/;
866 if ($line =~ m/^#(.*)\s*$/) {
870 if ($line =~ m/^(\S+)\s.*$/) {
872 $domains->{$domain} = {
873 domain
=> $domain, comment
=> $comment };
876 warn "parse error in '$filename': $line\n";
885 sub write_pmg_domains
{
886 my ($filename, $fh, $domains) = @_;
888 foreach my $domain (sort keys %$domains) {
889 my $comment = $domains->{$domain}->{comment
};
890 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
891 if defined($comment) && $comment !~ m/^\s*$/;
893 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
897 PVE
::INotify
::register_file
('domains', $domainsfilename,
900 undef, always_call_parser
=> 1);
902 my $mynetworks_filename = "/etc/pmg/mynetworks";
904 sub read_pmg_mynetworks
{
905 my ($filename, $fh) = @_;
911 while (defined(my $line = <$fh>)) {
913 next if $line =~ m/^\s*$/;
914 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
915 my ($network, $prefix_size, $comment) = ($1, $2, $3);
916 my $cidr = "$network/${prefix_size}";
917 $mynetworks->{$cidr} = {
919 network_address
=> $network,
920 prefix_size
=> $prefix_size,
921 comment
=> $comment // '',
924 warn "parse error in '$filename': $line\n";
932 sub write_pmg_mynetworks
{
933 my ($filename, $fh, $mynetworks) = @_;
935 foreach my $cidr (sort keys %$mynetworks) {
936 my $data = $mynetworks->{$cidr};
937 my $comment = $data->{comment
} // '*';
938 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
942 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
943 \
&read_pmg_mynetworks
,
944 \
&write_pmg_mynetworks
,
945 undef, always_call_parser
=> 1);
947 PVE
::JSONSchema
::register_format
(
948 'tls-policy', \
&pmg_verify_tls_policy
);
950 sub pmg_verify_tls_policy
{
951 my ($policy, $noerr) = @_;
953 # TODO: extend to parse attributes of the policy
954 my $valid_policy = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
956 if ($policy !~ /^${valid_policy}$/) {
957 return undef if $noerr;
958 die "value '$policy' does not look like a valid tls policy\n";
963 sub read_tls_policy
{
964 my ($filename, $fh) = @_;
966 return {} if !defined($fh);
970 while (defined(my $line = <$fh>)) {
972 next if $line =~ m/^\s*$/;
973 next if $line =~ m/^#(.*)\s*$/;
975 my $parse_error = sub {
977 warn "parse error in '$filename': $line - $err";
980 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
981 my ($domain, $policy) = ($1, $2);
984 pmg_verify_transport_domain
($domain);
985 pmg_verify_tls_policy
($policy);
988 $parse_error->($err);
992 $tls_policy->{$domain} = {
997 $parse_error->('wrong format');
1004 sub write_tls_policy
{
1005 my ($filename, $fh, $tls_policy) = @_;
1007 return if !$tls_policy;
1009 foreach my $domain (sort keys %$tls_policy) {
1010 my $entry = $tls_policy->{$domain};
1011 PVE
::Tools
::safe_print
(
1012 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1016 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1017 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1020 undef, always_call_parser
=> 1);
1022 sub postmap_tls_policy
{
1023 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1026 my $transport_map_filename = "/etc/pmg/transport";
1028 sub postmap_pmg_transport
{
1029 PMG
::Utils
::run_postmap
($transport_map_filename);
1032 sub read_transport_map
{
1033 my ($filename, $fh) = @_;
1035 return [] if !defined($fh);
1041 while (defined(my $line = <$fh>)) {
1043 next if $line =~ m/^\s*$/;
1044 if ($line =~ m/^#(.*)\s*$/) {
1049 my $parse_error = sub {
1051 warn "parse error in '$filename': $line - $err";
1055 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1056 my ($domain, $host, $port) = ($1, $2, $3);
1058 eval { pmg_verify_transport_domain_or_email
($domain); };
1060 $parse_error->($err);
1064 if ($host =~ m/^\[(.*)\]$/) {
1069 eval { PVE
::JSONSchema
::pve_verify_address
($host); };
1071 $parse_error->($err);
1080 comment
=> $comment,
1082 $res->{$domain} = $data;
1085 $parse_error->('wrong format');
1092 sub write_transport_map
{
1093 my ($filename, $fh, $tmap) = @_;
1097 foreach my $domain (sort keys %$tmap) {
1098 my $data = $tmap->{$domain};
1100 my $comment = $data->{comment
};
1101 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1102 if defined($comment) && $comment !~ m/^\s*$/;
1104 my $use_mx = $data->{use_mx
};
1105 $use_mx = 0 if $data->{host
} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1108 PVE
::Tools
::safe_print
(
1109 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1111 PVE
::Tools
::safe_print
(
1112 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1117 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1118 \
&read_transport_map
,
1119 \
&write_transport_map
,
1120 undef, always_call_parser
=> 1);
1122 # config file generation using templates
1124 sub get_template_vars
{
1127 my $vars = { pmg
=> $self->get_config() };
1129 my $nodename = PVE
::INotify
::nodename
();
1130 my $int_ip = PMG
::Cluster
::remote_node_ip
($nodename);
1131 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1133 my $transportnets = [];
1135 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1136 foreach my $domain (sort keys %$tmap) {
1137 my $data = $tmap->{$domain};
1138 my $host = $data->{host
};
1139 if ($host =~ m/^$IPV4RE$/) {
1140 push @$transportnets, "$host/32";
1141 } elsif ($host =~ m/^$IPV6RE$/) {
1142 push @$transportnets, "[$host]/128";
1147 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1149 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1151 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1152 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1153 push @$mynetworks, "[$1]/$2";
1155 push @$mynetworks, $int_net_cidr;
1158 if ($int_ip =~ m/^$IPV6RE$/) {
1159 push @$mynetworks, "[$int_ip]/128";
1161 push @$mynetworks, "$int_ip/32";
1165 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1166 foreach my $cidr (keys %$netlist) {
1167 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1168 push @$mynetworks, "[$1]/$2";
1170 push @$mynetworks, $cidr;
1174 push @$mynetworks, @$transportnets;
1176 # add default relay to mynetworks
1177 if (my $relay = $self->get('mail', 'relay')) {
1178 if ($relay =~ m/^$IPV4RE$/) {
1179 push @$mynetworks, "$relay/32";
1180 } elsif ($relay =~ m/^$IPV6RE$/) {
1181 push @$mynetworks, "[$relay]/128";
1183 # DNS name - do nothing ?
1187 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1189 # normalize dnsbl_sites
1190 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1191 if (scalar(@dnsbl_sites)) {
1192 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1195 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1198 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1199 $self->get('mail', 'spf');
1200 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1202 if ($int_ip =~ m/^$IPV6RE$/) {
1203 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1205 $vars->{postfix
}->{int_ip
} = $int_ip;
1208 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1209 $vars->{dns
}->{hostname
} = $nodename;
1211 my $domain = $resolv->{search
} // 'localdomain';
1212 $vars->{dns
}->{domain
} = $domain;
1214 my $wlbr = "$nodename.$domain";
1215 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1218 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1220 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1222 my $uri = URI-
>new($proxy);
1223 my $host = $uri->host;
1224 my $port = $uri->port // 8080;
1226 my $data = { host
=> $host, port
=> $port };
1227 if (my $ui = $uri->userinfo) {
1228 my ($username, $pw) = split(/:/, $ui, 2);
1229 $data->{username
} = $username;
1230 $data->{password
} = $pw if defined($pw);
1232 $vars->{proxy
} = $data;
1235 warn "parse http_proxy failed - $@" if $@;
1241 # use one global TT cache
1242 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1244 my $template_toolkit;
1246 sub get_template_toolkit
{
1248 return $template_toolkit if $template_toolkit;
1250 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1252 return $template_toolkit;
1255 # rewrite file from template
1256 # return true if file has changed
1257 sub rewrite_config_file
{
1258 my ($self, $tmplname, $dstfn) = @_;
1260 my $demo = $self->get('admin', 'demo');
1263 my $demosrc = "$tmplname.demo";
1264 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1267 my ($perm, $uid, $gid);
1269 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1270 # needed if file contains a HTTPProxyPasswort
1272 $uid = getpwnam('clamav');
1273 $gid = getgrnam('adm');
1277 my $tt = get_template_toolkit
();
1279 my $vars = $self->get_template_vars();
1283 $tt->process($tmplname, $vars, \
$output) ||
1284 die $tt->error() . "\n";
1286 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1288 return 0 if defined($old) && ($old eq $output); # no change
1290 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1292 if (defined($uid) && defined($gid)) {
1293 chown($uid, $gid, $dstfn);
1299 # rewrite spam configuration
1300 sub rewrite_config_spam
{
1303 my $use_awl = $self->get('spam', 'use_awl');
1304 my $use_bayes = $self->get('spam', 'use_bayes');
1305 my $use_razor = $self->get('spam', 'use_razor');
1309 # delete AW and bayes databases if those features are disabled
1311 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1315 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1316 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1317 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1320 # make sure we have a custom.cf file (else cluster sync fails)
1321 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1323 $changes = 1 if $self->rewrite_config_file(
1324 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1326 $changes = 1 if $self->rewrite_config_file(
1327 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1329 $changes = 1 if $self->rewrite_config_file(
1330 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1332 $changes = 1 if $self->rewrite_config_file(
1333 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1336 mkdir "/root/.razor";
1338 $changes = 1 if $self->rewrite_config_file(
1339 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1341 if (! -e
'/root/.razor/identity') {
1344 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1345 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1348 syslog
('info', "registering razor failed: $err") if $err;
1355 # rewrite ClamAV configuration
1356 sub rewrite_config_clam
{
1359 return $self->rewrite_config_file(
1360 'clamd.conf.in', '/etc/clamav/clamd.conf');
1363 sub rewrite_config_freshclam
{
1366 return $self->rewrite_config_file(
1367 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1370 sub rewrite_config_postgres
{
1373 my $pgconfdir = "/etc/postgresql/9.6/main";
1377 $changes = 1 if $self->rewrite_config_file(
1378 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1380 $changes = 1 if $self->rewrite_config_file(
1381 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1386 # rewrite /root/.forward
1387 sub rewrite_dot_forward
{
1390 my $dstfn = '/root/.forward';
1392 my $email = $self->get('admin', 'email');
1395 if ($email && $email =~ m/\s*(\S+)\s*/) {
1398 # empty .forward does not forward mails (see man local)
1401 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1403 return 0 if defined($old) && ($old eq $output); # no change
1405 PVE
::Tools
::file_set_contents
($dstfn, $output);
1410 my $write_smtp_whitelist = sub {
1411 my ($filename, $data, $action) = @_;
1413 $action = 'OK' if !$action;
1415 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1419 foreach my $k (sort keys %$data) {
1420 $new .= "$k $action\n";
1423 return 0 if defined($old) && ($old eq $new); # no change
1425 PVE
::Tools
::file_set_contents
($filename, $new);
1427 PMG
::Utils
::run_postmap
($filename);
1432 sub rewrite_postfix_whitelist
{
1433 my ($rulecache) = @_;
1435 # see man page for regexp_table for postfix regex table format
1437 # we use a hash to avoid duplicate entries in regex tables
1440 my $clientlist = {};
1442 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1443 my $oclass = ref($obj);
1444 if ($oclass eq 'PMG::RuleDB::Receiver') {
1445 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1446 $tolist->{"/^$addr\$/"} = 1;
1447 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1448 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1449 $tolist->{"/^.+\@$addr\$/"} = 1;
1450 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1451 my $addr = $obj->{address
};
1453 $tolist->{"/^$addr\$/"} = 1;
1457 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1458 my $oclass = ref($obj);
1459 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1460 if ($oclass eq 'PMG::RuleDB::EMail') {
1461 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1462 $fromlist->{"/^$addr\$/"} = 1;
1463 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1464 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1465 $fromlist->{"/^.+\@$addr\$/"} = 1;
1466 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1467 my $addr = $obj->{address
};
1469 $fromlist->{"/^$addr\$/"} = 1;
1470 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1471 $clientlist->{$obj->{address
}} = 1;
1472 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1473 $clientlist->{$obj->{address
}} = 1;
1477 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1478 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1479 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1480 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1483 # rewrite /etc/postfix/*
1484 sub rewrite_config_postfix
{
1485 my ($self, $rulecache) = @_;
1487 # make sure we have required files (else postfix start fails)
1488 IO
::File-
>new($transport_map_filename, 'a', 0644);
1492 if ($self->get('mail', 'tls')) {
1494 PMG
::Utils
::gen_proxmox_tls_cert
();
1496 syslog
('info', "generating certificate failed: $@") if $@;
1499 $changes = 1 if $self->rewrite_config_file(
1500 'main.cf.in', '/etc/postfix/main.cf');
1502 $changes = 1 if $self->rewrite_config_file(
1503 'master.cf.in', '/etc/postfix/master.cf');
1505 # make sure we have required files (else postfix start fails)
1506 # Note: postmap need a valid /etc/postfix/main.cf configuration
1507 postmap_pmg_domains
();
1508 postmap_pmg_transport
();
1509 postmap_tls_policy
();
1511 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1513 # make sure aliases.db is up to date
1514 system('/usr/bin/newaliases');
1519 sub rewrite_config
{
1520 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1522 $force_restart = {} if ! $force_restart;
1524 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1525 $force_restart->{postfix
}) {
1526 PMG
::Utils
::service_cmd
('postfix', 'restart');
1529 if ($self->rewrite_dot_forward() && $restart_services) {
1530 # no need to restart anything
1533 if ($self->rewrite_config_postgres() && $restart_services) {
1534 # do nothing (too many side effects)?
1535 # does not happen anyways, because config does not change.
1538 if (($self->rewrite_config_spam() && $restart_services) ||
1539 $force_restart->{spam
}) {
1540 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1543 if (($self->rewrite_config_clam() && $restart_services) ||
1544 $force_restart->{clam
}) {
1545 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1548 if (($self->rewrite_config_freshclam() && $restart_services) ||
1549 $force_restart->{freshclam
}) {
1550 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');