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 # TODO: extend to parse attributes of the policy
951 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
952 sub pmg_verify_tls_policy
{
953 my ($policy, $noerr) = @_;
955 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
956 return undef if $noerr;
957 die "value '$policy' does not look like a valid tls policy\n";
962 sub pmg_verify_tls_policy_strict
{
966 if ($policy =~ /^$VALID_TLS_POLICY_RE$/);
971 sub read_tls_policy
{
972 my ($filename, $fh) = @_;
974 return {} if !defined($fh);
978 while (defined(my $line = <$fh>)) {
980 next if $line =~ m/^\s*$/;
981 next if $line =~ m/^#(.*)\s*$/;
983 my $parse_error = sub {
985 die "parse error in '$filename': $line - $err";
988 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
989 my ($domain, $policy) = ($1, $2);
992 pmg_verify_transport_domain
($domain);
993 pmg_verify_tls_policy
($policy);
996 $parse_error->($err);
1000 $tls_policy->{$domain} = {
1005 $parse_error->('wrong format');
1012 sub write_tls_policy
{
1013 my ($filename, $fh, $tls_policy) = @_;
1015 return if !$tls_policy;
1017 foreach my $domain (sort keys %$tls_policy) {
1018 my $entry = $tls_policy->{$domain};
1019 PVE
::Tools
::safe_print
(
1020 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1024 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1025 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1028 undef, always_call_parser
=> 1);
1030 sub postmap_tls_policy
{
1031 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1034 my $transport_map_filename = "/etc/pmg/transport";
1036 sub postmap_pmg_transport
{
1037 PMG
::Utils
::run_postmap
($transport_map_filename);
1040 sub read_transport_map
{
1041 my ($filename, $fh) = @_;
1043 return [] if !defined($fh);
1049 while (defined(my $line = <$fh>)) {
1051 next if $line =~ m/^\s*$/;
1052 if ($line =~ m/^#(.*)\s*$/) {
1057 my $parse_error = sub {
1059 warn "parse error in '$filename': $line - $err";
1063 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1064 my ($domain, $host, $port) = ($1, $2, $3);
1066 eval { pmg_verify_transport_domain_or_email
($domain); };
1068 $parse_error->($err);
1072 if ($host =~ m/^\[(.*)\]$/) {
1077 eval { PVE
::JSONSchema
::pve_verify_address
($host); };
1079 $parse_error->($err);
1088 comment
=> $comment,
1090 $res->{$domain} = $data;
1093 $parse_error->('wrong format');
1100 sub write_transport_map
{
1101 my ($filename, $fh, $tmap) = @_;
1105 foreach my $domain (sort keys %$tmap) {
1106 my $data = $tmap->{$domain};
1108 my $comment = $data->{comment
};
1109 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1110 if defined($comment) && $comment !~ m/^\s*$/;
1112 my $use_mx = $data->{use_mx
};
1113 $use_mx = 0 if $data->{host
} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1116 PVE
::Tools
::safe_print
(
1117 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1119 PVE
::Tools
::safe_print
(
1120 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1125 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1126 \
&read_transport_map
,
1127 \
&write_transport_map
,
1128 undef, always_call_parser
=> 1);
1130 # config file generation using templates
1132 sub get_template_vars
{
1135 my $vars = { pmg
=> $self->get_config() };
1137 my $nodename = PVE
::INotify
::nodename
();
1138 my $int_ip = PMG
::Cluster
::remote_node_ip
($nodename);
1139 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1141 my $transportnets = [];
1143 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1144 foreach my $domain (sort keys %$tmap) {
1145 my $data = $tmap->{$domain};
1146 my $host = $data->{host
};
1147 if ($host =~ m/^$IPV4RE$/) {
1148 push @$transportnets, "$host/32";
1149 } elsif ($host =~ m/^$IPV6RE$/) {
1150 push @$transportnets, "[$host]/128";
1155 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1157 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1159 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1160 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1161 push @$mynetworks, "[$1]/$2";
1163 push @$mynetworks, $int_net_cidr;
1166 if ($int_ip =~ m/^$IPV6RE$/) {
1167 push @$mynetworks, "[$int_ip]/128";
1169 push @$mynetworks, "$int_ip/32";
1173 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1174 foreach my $cidr (keys %$netlist) {
1175 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1176 push @$mynetworks, "[$1]/$2";
1178 push @$mynetworks, $cidr;
1182 push @$mynetworks, @$transportnets;
1184 # add default relay to mynetworks
1185 if (my $relay = $self->get('mail', 'relay')) {
1186 if ($relay =~ m/^$IPV4RE$/) {
1187 push @$mynetworks, "$relay/32";
1188 } elsif ($relay =~ m/^$IPV6RE$/) {
1189 push @$mynetworks, "[$relay]/128";
1191 # DNS name - do nothing ?
1195 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1197 # normalize dnsbl_sites
1198 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1199 if (scalar(@dnsbl_sites)) {
1200 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1203 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1206 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1207 $self->get('mail', 'spf');
1208 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1210 if ($int_ip =~ m/^$IPV6RE$/) {
1211 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1213 $vars->{postfix
}->{int_ip
} = $int_ip;
1216 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1217 $vars->{dns
}->{hostname
} = $nodename;
1219 my $domain = $resolv->{search
} // 'localdomain';
1220 $vars->{dns
}->{domain
} = $domain;
1222 my $wlbr = "$nodename.$domain";
1223 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1226 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1228 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1230 my $uri = URI-
>new($proxy);
1231 my $host = $uri->host;
1232 my $port = $uri->port // 8080;
1234 my $data = { host
=> $host, port
=> $port };
1235 if (my $ui = $uri->userinfo) {
1236 my ($username, $pw) = split(/:/, $ui, 2);
1237 $data->{username
} = $username;
1238 $data->{password
} = $pw if defined($pw);
1240 $vars->{proxy
} = $data;
1243 warn "parse http_proxy failed - $@" if $@;
1249 # use one global TT cache
1250 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1252 my $template_toolkit;
1254 sub get_template_toolkit
{
1256 return $template_toolkit if $template_toolkit;
1258 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1260 return $template_toolkit;
1263 # rewrite file from template
1264 # return true if file has changed
1265 sub rewrite_config_file
{
1266 my ($self, $tmplname, $dstfn) = @_;
1268 my $demo = $self->get('admin', 'demo');
1271 my $demosrc = "$tmplname.demo";
1272 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1275 my ($perm, $uid, $gid);
1277 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1278 # needed if file contains a HTTPProxyPasswort
1280 $uid = getpwnam('clamav');
1281 $gid = getgrnam('adm');
1285 my $tt = get_template_toolkit
();
1287 my $vars = $self->get_template_vars();
1291 $tt->process($tmplname, $vars, \
$output) ||
1292 die $tt->error() . "\n";
1294 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1296 return 0 if defined($old) && ($old eq $output); # no change
1298 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1300 if (defined($uid) && defined($gid)) {
1301 chown($uid, $gid, $dstfn);
1307 # rewrite spam configuration
1308 sub rewrite_config_spam
{
1311 my $use_awl = $self->get('spam', 'use_awl');
1312 my $use_bayes = $self->get('spam', 'use_bayes');
1313 my $use_razor = $self->get('spam', 'use_razor');
1317 # delete AW and bayes databases if those features are disabled
1319 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1323 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1324 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1325 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1328 # make sure we have a custom.cf file (else cluster sync fails)
1329 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1331 $changes = 1 if $self->rewrite_config_file(
1332 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1334 $changes = 1 if $self->rewrite_config_file(
1335 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1337 $changes = 1 if $self->rewrite_config_file(
1338 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1340 $changes = 1 if $self->rewrite_config_file(
1341 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1344 mkdir "/root/.razor";
1346 $changes = 1 if $self->rewrite_config_file(
1347 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1349 if (! -e
'/root/.razor/identity') {
1352 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1353 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1356 syslog
('info', "registering razor failed: $err") if $err;
1363 # rewrite ClamAV configuration
1364 sub rewrite_config_clam
{
1367 return $self->rewrite_config_file(
1368 'clamd.conf.in', '/etc/clamav/clamd.conf');
1371 sub rewrite_config_freshclam
{
1374 return $self->rewrite_config_file(
1375 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1378 sub rewrite_config_postgres
{
1381 my $pgconfdir = "/etc/postgresql/9.6/main";
1385 $changes = 1 if $self->rewrite_config_file(
1386 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1388 $changes = 1 if $self->rewrite_config_file(
1389 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1394 # rewrite /root/.forward
1395 sub rewrite_dot_forward
{
1398 my $dstfn = '/root/.forward';
1400 my $email = $self->get('admin', 'email');
1403 if ($email && $email =~ m/\s*(\S+)\s*/) {
1406 # empty .forward does not forward mails (see man local)
1409 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1411 return 0 if defined($old) && ($old eq $output); # no change
1413 PVE
::Tools
::file_set_contents
($dstfn, $output);
1418 my $write_smtp_whitelist = sub {
1419 my ($filename, $data, $action) = @_;
1421 $action = 'OK' if !$action;
1423 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1427 foreach my $k (sort keys %$data) {
1428 $new .= "$k $action\n";
1431 return 0 if defined($old) && ($old eq $new); # no change
1433 PVE
::Tools
::file_set_contents
($filename, $new);
1435 PMG
::Utils
::run_postmap
($filename);
1440 sub rewrite_postfix_whitelist
{
1441 my ($rulecache) = @_;
1443 # see man page for regexp_table for postfix regex table format
1445 # we use a hash to avoid duplicate entries in regex tables
1448 my $clientlist = {};
1450 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1451 my $oclass = ref($obj);
1452 if ($oclass eq 'PMG::RuleDB::Receiver') {
1453 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1454 $tolist->{"/^$addr\$/"} = 1;
1455 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1456 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1457 $tolist->{"/^.+\@$addr\$/"} = 1;
1458 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1459 my $addr = $obj->{address
};
1461 $tolist->{"/^$addr\$/"} = 1;
1465 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1466 my $oclass = ref($obj);
1467 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1468 if ($oclass eq 'PMG::RuleDB::EMail') {
1469 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1470 $fromlist->{"/^$addr\$/"} = 1;
1471 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1472 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1473 $fromlist->{"/^.+\@$addr\$/"} = 1;
1474 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1475 my $addr = $obj->{address
};
1477 $fromlist->{"/^$addr\$/"} = 1;
1478 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1479 $clientlist->{$obj->{address
}} = 1;
1480 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1481 $clientlist->{$obj->{address
}} = 1;
1485 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1486 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1487 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1488 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1491 # rewrite /etc/postfix/*
1492 sub rewrite_config_postfix
{
1493 my ($self, $rulecache) = @_;
1495 # make sure we have required files (else postfix start fails)
1496 IO
::File-
>new($transport_map_filename, 'a', 0644);
1500 if ($self->get('mail', 'tls')) {
1502 PMG
::Utils
::gen_proxmox_tls_cert
();
1504 syslog
('info', "generating certificate failed: $@") if $@;
1507 $changes = 1 if $self->rewrite_config_file(
1508 'main.cf.in', '/etc/postfix/main.cf');
1510 $changes = 1 if $self->rewrite_config_file(
1511 'master.cf.in', '/etc/postfix/master.cf');
1513 # make sure we have required files (else postfix start fails)
1514 # Note: postmap need a valid /etc/postfix/main.cf configuration
1515 postmap_pmg_domains
();
1516 postmap_pmg_transport
();
1517 postmap_tls_policy
();
1519 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1521 # make sure aliases.db is up to date
1522 system('/usr/bin/newaliases');
1527 sub rewrite_config
{
1528 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1530 $force_restart = {} if ! $force_restart;
1532 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1533 $force_restart->{postfix
}) {
1534 PMG
::Utils
::service_cmd
('postfix', 'restart');
1537 if ($self->rewrite_dot_forward() && $restart_services) {
1538 # no need to restart anything
1541 if ($self->rewrite_config_postgres() && $restart_services) {
1542 # do nothing (too many side effects)?
1543 # does not happen anyways, because config does not change.
1546 if (($self->rewrite_config_spam() && $restart_services) ||
1547 $force_restart->{spam
}) {
1548 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1551 if (($self->rewrite_config_clam() && $restart_services) ||
1552 $force_restart->{clam
}) {
1553 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1556 if (($self->rewrite_config_freshclam() && $restart_services) ||
1557 $force_restart->{freshclam
}) {
1558 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');