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 PVE
::JSONSchema
::register_format
(
963 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
965 sub pmg_verify_tls_policy_strict
{
966 my ($policy, $noerr) = @_;
968 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
969 return undef if $noerr;
970 die "value '$policy' does not look like a valid tls policy\n";
975 sub read_tls_policy
{
976 my ($filename, $fh) = @_;
978 return {} if !defined($fh);
982 while (defined(my $line = <$fh>)) {
984 next if $line =~ m/^\s*$/;
985 next if $line =~ m/^#(.*)\s*$/;
987 my $parse_error = sub {
989 die "parse error in '$filename': $line - $err";
992 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
993 my ($domain, $policy) = ($1, $2);
996 pmg_verify_transport_domain
($domain);
997 pmg_verify_tls_policy
($policy);
1000 $parse_error->($err);
1004 $tls_policy->{$domain} = {
1009 $parse_error->('wrong format');
1016 sub write_tls_policy
{
1017 my ($filename, $fh, $tls_policy) = @_;
1019 return if !$tls_policy;
1021 foreach my $domain (sort keys %$tls_policy) {
1022 my $entry = $tls_policy->{$domain};
1023 PVE
::Tools
::safe_print
(
1024 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1028 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1029 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1032 undef, always_call_parser
=> 1);
1034 sub postmap_tls_policy
{
1035 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1038 my $transport_map_filename = "/etc/pmg/transport";
1040 sub postmap_pmg_transport
{
1041 PMG
::Utils
::run_postmap
($transport_map_filename);
1044 sub read_transport_map
{
1045 my ($filename, $fh) = @_;
1047 return [] if !defined($fh);
1053 while (defined(my $line = <$fh>)) {
1055 next if $line =~ m/^\s*$/;
1056 if ($line =~ m/^#(.*)\s*$/) {
1061 my $parse_error = sub {
1063 warn "parse error in '$filename': $line - $err";
1067 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1068 my ($domain, $host, $port) = ($1, $2, $3);
1070 eval { pmg_verify_transport_domain_or_email
($domain); };
1072 $parse_error->($err);
1076 if ($host =~ m/^\[(.*)\]$/) {
1081 eval { PVE
::JSONSchema
::pve_verify_address
($host); };
1083 $parse_error->($err);
1092 comment
=> $comment,
1094 $res->{$domain} = $data;
1097 $parse_error->('wrong format');
1104 sub write_transport_map
{
1105 my ($filename, $fh, $tmap) = @_;
1109 foreach my $domain (sort keys %$tmap) {
1110 my $data = $tmap->{$domain};
1112 my $comment = $data->{comment
};
1113 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1114 if defined($comment) && $comment !~ m/^\s*$/;
1116 my $use_mx = $data->{use_mx
};
1117 $use_mx = 0 if $data->{host
} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1120 PVE
::Tools
::safe_print
(
1121 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1123 PVE
::Tools
::safe_print
(
1124 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1129 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1130 \
&read_transport_map
,
1131 \
&write_transport_map
,
1132 undef, always_call_parser
=> 1);
1134 # config file generation using templates
1136 sub get_template_vars
{
1139 my $vars = { pmg
=> $self->get_config() };
1141 my $nodename = PVE
::INotify
::nodename
();
1142 my $int_ip = PMG
::Cluster
::remote_node_ip
($nodename);
1143 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1145 my $transportnets = [];
1147 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1148 foreach my $domain (sort keys %$tmap) {
1149 my $data = $tmap->{$domain};
1150 my $host = $data->{host
};
1151 if ($host =~ m/^$IPV4RE$/) {
1152 push @$transportnets, "$host/32";
1153 } elsif ($host =~ m/^$IPV6RE$/) {
1154 push @$transportnets, "[$host]/128";
1159 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1161 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1163 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1164 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1165 push @$mynetworks, "[$1]/$2";
1167 push @$mynetworks, $int_net_cidr;
1170 if ($int_ip =~ m/^$IPV6RE$/) {
1171 push @$mynetworks, "[$int_ip]/128";
1173 push @$mynetworks, "$int_ip/32";
1177 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1178 foreach my $cidr (keys %$netlist) {
1179 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1180 push @$mynetworks, "[$1]/$2";
1182 push @$mynetworks, $cidr;
1186 push @$mynetworks, @$transportnets;
1188 # add default relay to mynetworks
1189 if (my $relay = $self->get('mail', 'relay')) {
1190 if ($relay =~ m/^$IPV4RE$/) {
1191 push @$mynetworks, "$relay/32";
1192 } elsif ($relay =~ m/^$IPV6RE$/) {
1193 push @$mynetworks, "[$relay]/128";
1195 # DNS name - do nothing ?
1199 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1201 # normalize dnsbl_sites
1202 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1203 if (scalar(@dnsbl_sites)) {
1204 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1207 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1210 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1211 $self->get('mail', 'spf');
1212 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1214 if ($int_ip =~ m/^$IPV6RE$/) {
1215 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1217 $vars->{postfix
}->{int_ip
} = $int_ip;
1220 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1221 $vars->{dns
}->{hostname
} = $nodename;
1223 my $domain = $resolv->{search
} // 'localdomain';
1224 $vars->{dns
}->{domain
} = $domain;
1226 my $wlbr = "$nodename.$domain";
1227 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1230 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1232 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1234 my $uri = URI-
>new($proxy);
1235 my $host = $uri->host;
1236 my $port = $uri->port // 8080;
1238 my $data = { host
=> $host, port
=> $port };
1239 if (my $ui = $uri->userinfo) {
1240 my ($username, $pw) = split(/:/, $ui, 2);
1241 $data->{username
} = $username;
1242 $data->{password
} = $pw if defined($pw);
1244 $vars->{proxy
} = $data;
1247 warn "parse http_proxy failed - $@" if $@;
1253 # use one global TT cache
1254 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1256 my $template_toolkit;
1258 sub get_template_toolkit
{
1260 return $template_toolkit if $template_toolkit;
1262 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1264 return $template_toolkit;
1267 # rewrite file from template
1268 # return true if file has changed
1269 sub rewrite_config_file
{
1270 my ($self, $tmplname, $dstfn) = @_;
1272 my $demo = $self->get('admin', 'demo');
1275 my $demosrc = "$tmplname.demo";
1276 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1279 my ($perm, $uid, $gid);
1281 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1282 # needed if file contains a HTTPProxyPasswort
1284 $uid = getpwnam('clamav');
1285 $gid = getgrnam('adm');
1289 my $tt = get_template_toolkit
();
1291 my $vars = $self->get_template_vars();
1295 $tt->process($tmplname, $vars, \
$output) ||
1296 die $tt->error() . "\n";
1298 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1300 return 0 if defined($old) && ($old eq $output); # no change
1302 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1304 if (defined($uid) && defined($gid)) {
1305 chown($uid, $gid, $dstfn);
1311 # rewrite spam configuration
1312 sub rewrite_config_spam
{
1315 my $use_awl = $self->get('spam', 'use_awl');
1316 my $use_bayes = $self->get('spam', 'use_bayes');
1317 my $use_razor = $self->get('spam', 'use_razor');
1321 # delete AW and bayes databases if those features are disabled
1323 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1327 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1328 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1329 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1332 # make sure we have a custom.cf file (else cluster sync fails)
1333 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1335 $changes = 1 if $self->rewrite_config_file(
1336 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1338 $changes = 1 if $self->rewrite_config_file(
1339 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1341 $changes = 1 if $self->rewrite_config_file(
1342 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1344 $changes = 1 if $self->rewrite_config_file(
1345 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1348 mkdir "/root/.razor";
1350 $changes = 1 if $self->rewrite_config_file(
1351 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1353 if (! -e
'/root/.razor/identity') {
1356 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1357 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1360 syslog
('info', "registering razor failed: $err") if $err;
1367 # rewrite ClamAV configuration
1368 sub rewrite_config_clam
{
1371 return $self->rewrite_config_file(
1372 'clamd.conf.in', '/etc/clamav/clamd.conf');
1375 sub rewrite_config_freshclam
{
1378 return $self->rewrite_config_file(
1379 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1382 sub rewrite_config_postgres
{
1385 my $pgconfdir = "/etc/postgresql/9.6/main";
1389 $changes = 1 if $self->rewrite_config_file(
1390 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1392 $changes = 1 if $self->rewrite_config_file(
1393 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1398 # rewrite /root/.forward
1399 sub rewrite_dot_forward
{
1402 my $dstfn = '/root/.forward';
1404 my $email = $self->get('admin', 'email');
1407 if ($email && $email =~ m/\s*(\S+)\s*/) {
1410 # empty .forward does not forward mails (see man local)
1413 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1415 return 0 if defined($old) && ($old eq $output); # no change
1417 PVE
::Tools
::file_set_contents
($dstfn, $output);
1422 my $write_smtp_whitelist = sub {
1423 my ($filename, $data, $action) = @_;
1425 $action = 'OK' if !$action;
1427 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1431 foreach my $k (sort keys %$data) {
1432 $new .= "$k $action\n";
1435 return 0 if defined($old) && ($old eq $new); # no change
1437 PVE
::Tools
::file_set_contents
($filename, $new);
1439 PMG
::Utils
::run_postmap
($filename);
1444 sub rewrite_postfix_whitelist
{
1445 my ($rulecache) = @_;
1447 # see man page for regexp_table for postfix regex table format
1449 # we use a hash to avoid duplicate entries in regex tables
1452 my $clientlist = {};
1454 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1455 my $oclass = ref($obj);
1456 if ($oclass eq 'PMG::RuleDB::Receiver') {
1457 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1458 $tolist->{"/^$addr\$/"} = 1;
1459 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1460 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1461 $tolist->{"/^.+\@$addr\$/"} = 1;
1462 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1463 my $addr = $obj->{address
};
1465 $tolist->{"/^$addr\$/"} = 1;
1469 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1470 my $oclass = ref($obj);
1471 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1472 if ($oclass eq 'PMG::RuleDB::EMail') {
1473 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1474 $fromlist->{"/^$addr\$/"} = 1;
1475 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1476 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1477 $fromlist->{"/^.+\@$addr\$/"} = 1;
1478 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1479 my $addr = $obj->{address
};
1481 $fromlist->{"/^$addr\$/"} = 1;
1482 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1483 $clientlist->{$obj->{address
}} = 1;
1484 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1485 $clientlist->{$obj->{address
}} = 1;
1489 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1490 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1491 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1492 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1495 # rewrite /etc/postfix/*
1496 sub rewrite_config_postfix
{
1497 my ($self, $rulecache) = @_;
1499 # make sure we have required files (else postfix start fails)
1500 IO
::File-
>new($transport_map_filename, 'a', 0644);
1504 if ($self->get('mail', 'tls')) {
1506 PMG
::Utils
::gen_proxmox_tls_cert
();
1508 syslog
('info', "generating certificate failed: $@") if $@;
1511 $changes = 1 if $self->rewrite_config_file(
1512 'main.cf.in', '/etc/postfix/main.cf');
1514 $changes = 1 if $self->rewrite_config_file(
1515 'master.cf.in', '/etc/postfix/master.cf');
1517 # make sure we have required files (else postfix start fails)
1518 # Note: postmap need a valid /etc/postfix/main.cf configuration
1519 postmap_pmg_domains
();
1520 postmap_pmg_transport
();
1521 postmap_tls_policy
();
1523 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1525 # make sure aliases.db is up to date
1526 system('/usr/bin/newaliases');
1531 sub rewrite_config
{
1532 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1534 $force_restart = {} if ! $force_restart;
1536 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1537 $force_restart->{postfix
}) {
1538 PMG
::Utils
::service_cmd
('postfix', 'restart');
1541 if ($self->rewrite_dot_forward() && $restart_services) {
1542 # no need to restart anything
1545 if ($self->rewrite_config_postgres() && $restart_services) {
1546 # do nothing (too many side effects)?
1547 # does not happen anyways, because config does not change.
1550 if (($self->rewrite_config_spam() && $restart_services) ||
1551 $force_restart->{spam
}) {
1552 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1555 if (($self->rewrite_config_clam() && $restart_services) ||
1556 $force_restart->{clam
}) {
1557 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1560 if (($self->rewrite_config_freshclam() && $restart_services) ||
1561 $force_restart->{freshclam
}) {
1562 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');