1 package PMG
::Config
::Base
;
9 use PVE
::JSONSchema
qw(get_standard_option);
10 use PVE
::SectionConfig
;
12 use base
qw(PVE::SectionConfig);
16 type
=> { description
=> "Section type." },
18 description
=> "Section ID.",
19 type
=> 'string', format
=> 'pve-configid',
28 sub format_section_header
{
29 my ($class, $type, $sectionId) = @_;
31 die "internal error ($type ne $sectionId)" if $type ne $sectionId;
33 return "section: $type\n";
37 sub parse_section_header
{
38 my ($class, $line) = @_;
40 if ($line =~ m/^section:\s*(\S+)\s*$/) {
42 my $errmsg = undef; # set if you want to skip whole section
43 eval { PVE
::JSONSchema
::pve_verify_configid
($section); };
45 my $config = {}; # to return additional attributes
46 return ($section, $section, $errmsg, $config);
51 package PMG
::Config
::Admin
;
56 use base
qw(PMG::Config::Base);
65 description
=> "Use advanced filters for statistic.",
70 description
=> "Send daily reports.",
75 description
=> "User Statistics Lifetime (days)",
81 description
=> "Demo mode - do not start SMTP filter.",
86 description
=> "Administrator E-Mail address.",
87 type
=> 'string', format
=> 'email',
88 default => 'admin@domain.tld',
91 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
93 pattern
=> "http://.*",
96 description
=> "Use Avast Virus Scanner (/usr/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.",
101 description
=> "Use ClamAV Virus Scanner. This is the default virus scanner and is enabled by default.",
106 description
=> "Use Custom Check Script. The script has to take the defined arguments and can return Virus findings or a Spamscore.",
110 custom_check_path
=> {
111 description
=> "Absolute Path to the Custom Check Script",
112 type
=> 'string', pattern
=> '^/([^/\0]+\/)+[^/\0]+$',
113 default => '/usr/local/bin/pmg-custom-check',
116 description
=> "DKIM sign outbound mails with the configured Selector.",
120 dkim_sign_all_mail
=> {
121 description
=> "DKIM sign all outgoing mails irrespective of the Envelope From domain.",
126 description
=> "Default DKIM selector",
127 type
=> 'string', format
=> 'dns-name', #see RFC6376 3.1
134 advfilter
=> { optional
=> 1 },
135 avast
=> { optional
=> 1 },
136 clamav
=> { optional
=> 1 },
137 statlifetime
=> { optional
=> 1 },
138 dailyreport
=> { optional
=> 1 },
139 demo
=> { optional
=> 1 },
140 email
=> { optional
=> 1 },
141 http_proxy
=> { optional
=> 1 },
142 custom_check
=> { optional
=> 1 },
143 custom_check_path
=> { optional
=> 1 },
144 dkim_sign
=> { optional
=> 1 },
145 dkim_sign_all_mail
=> { optional
=> 1 },
146 dkim_selector
=> { optional
=> 1 },
150 package PMG
::Config
::Spam
;
155 use base
qw(PMG::Config::Base);
164 description
=> "This option is used to specify which languages are considered OK for incoming mail.",
166 pattern
=> '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
170 description
=> "Whether to use the naive-Bayesian-style classifier.",
175 description
=> "Use the Auto-Whitelist plugin.",
180 description
=> "Whether to use Razor2, if it is available.",
184 wl_bounce_relays
=> {
185 description
=> "Whitelist legitimate bounce relays.",
188 clamav_heuristic_score
=> {
189 description
=> "Score for ClamAV heuristics (Encrypted Archives/Documents, Google Safe Browsing database, PhishingScanURLs, ...).",
196 description
=> "Additional score for bounce mails.",
203 description
=> "Enable real time blacklists (RBL) checks.",
208 description
=> "Maximum size of spam messages in bytes.",
218 use_awl
=> { optional
=> 1 },
219 use_razor
=> { optional
=> 1 },
220 wl_bounce_relays
=> { optional
=> 1 },
221 languages
=> { optional
=> 1 },
222 use_bayes
=> { optional
=> 1 },
223 clamav_heuristic_score
=> { optional
=> 1 },
224 bounce_score
=> { optional
=> 1 },
225 rbl_checks
=> { optional
=> 1 },
226 maxspamsize
=> { optional
=> 1 },
230 package PMG
::Config
::SpamQuarantine
;
235 use base
qw(PMG::Config::Base);
244 description
=> "Quarantine life time (days)",
250 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.",
252 enum
=> [qw(ticket ldap ldapticket)],
256 description
=> "Spam report style.",
258 enum
=> [qw(none short verbose custom)],
259 default => 'verbose',
262 description
=> "Allow to view images.",
267 description
=> "Allow to view hyperlinks.",
272 description
=> "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
273 type
=> 'string', format
=> 'address',
276 description
=> "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
283 description
=> "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
285 enum
=> [qw(http https)],
289 description
=> "Text for 'From' header in daily spam report mails.",
297 mailfrom
=> { optional
=> 1 },
298 hostname
=> { optional
=> 1 },
299 lifetime
=> { optional
=> 1 },
300 authmode
=> { optional
=> 1 },
301 reportstyle
=> { optional
=> 1 },
302 viewimages
=> { optional
=> 1 },
303 allowhrefs
=> { optional
=> 1 },
304 port
=> { optional
=> 1 },
305 protocol
=> { optional
=> 1 },
309 package PMG
::Config
::VirusQuarantine
;
314 use base
qw(PMG::Config::Base);
326 lifetime
=> { optional
=> 1 },
327 viewimages
=> { optional
=> 1 },
328 allowhrefs
=> { optional
=> 1 },
332 package PMG
::Config
::ClamAV
;
337 use base
qw(PMG::Config::Base);
346 description
=> "ClamAV database mirror server.",
348 default => 'database.clamav.net',
350 archiveblockencrypted
=> {
351 description
=> "Whether to mark encrypted archives and documents as heuristic virus match. A match does not necessarily result in an immediate block, it just raises the Spam Score by 'clamav_heuristic_score'.",
356 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.",
362 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.",
368 description
=> "Files larger than this limit (in bytes) won't be scanned.",
374 description
=> "Sets the maximum amount of data (in bytes) to be scanned for each input file.",
377 default => 100000000,
380 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
386 description
=> "Enables support for Google Safe Browsing.",
395 archiveblockencrypted
=> { optional
=> 1 },
396 archivemaxrec
=> { optional
=> 1 },
397 archivemaxfiles
=> { optional
=> 1 },
398 archivemaxsize
=> { optional
=> 1 },
399 maxscansize
=> { optional
=> 1 },
400 dbmirror
=> { optional
=> 1 },
401 maxcccount
=> { optional
=> 1 },
402 safebrowsing
=> { optional
=> 1 },
406 package PMG
::Config
::Mail
;
411 use PVE
::ProcFSTools
;
413 use base
qw(PMG::Config::Base);
420 sub physical_memory
{
422 return $physicalmem if $physicalmem;
424 my $info = PVE
::ProcFSTools
::read_meminfo
();
425 my $total = int($info->{memtotal
} / (1024*1024));
430 sub get_max_filters
{
431 # estimate optimal number of filter servers
435 my $memory = physical_memory
();
436 my $add_servers = int(($memory - 512)/$servermem);
437 $max_servers += $add_servers if $add_servers > 0;
438 $max_servers = 40 if $max_servers > 40;
440 return $max_servers - 2;
444 # estimate optimal number of smtpd daemons
446 my $max_servers = 25;
448 my $memory = physical_memory
();
449 my $add_servers = int(($memory - 512)/$servermem);
450 $max_servers += $add_servers if $add_servers > 0;
451 $max_servers = 100 if $max_servers > 100;
456 # estimate optimal number of proxpolicy servers
458 my $memory = physical_memory
();
459 $max_servers = 5 if $memory >= 500;
466 description
=> "SMTP port number for outgoing mail (trusted).",
473 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
480 description
=> "The default mail delivery transport (incoming mails).",
481 type
=> 'string', format
=> 'address',
484 description
=> "SMTP port number for relay host.",
491 description
=> "Disable MX lookups for default relay.",
496 description
=> "When set, all outgoing mails are deliverd to the specified smarthost.",
497 type
=> 'string', format
=> 'address',
500 description
=> "SMTP port number for smarthost.",
507 description
=> "ESMTP banner.",
510 default => 'ESMTP Proxmox',
513 description
=> "Maximum number of pmg-smtp-filter processes.",
517 default => get_max_filters
(),
520 description
=> "Maximum number of pmgpolicy processes.",
524 default => get_max_policy
(),
527 description
=> "Maximum number of SMTP daemon processes (in).",
531 default => get_max_smtpd
(),
534 description
=> "Maximum number of SMTP daemon processes (out).",
538 default => get_max_smtpd
(),
540 conn_count_limit
=> {
541 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
547 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.",
552 message_rate_limit
=> {
553 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.",
559 description
=> "Hide received header in outgoing mails.",
564 description
=> "Maximum email size. Larger mails are rejected.",
567 default => 1024*1024*10,
570 description
=> "SMTP delay warning time (in hours).",
576 description
=> "Enable TLS.",
581 description
=> "Enable TLS Logging.",
586 description
=> "Add TLS received header.",
591 description
=> "Use Sender Policy Framework.",
596 description
=> "Use Greylisting.",
601 description
=> "Use SMTP HELO tests.",
606 description
=> "Reject unknown clients.",
610 rejectunknownsender
=> {
611 description
=> "Reject unknown senders.",
616 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
618 enum
=> ['450', '550'],
621 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
622 type
=> 'string', format
=> 'dnsbl-entry-list',
625 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
635 int_port
=> { optional
=> 1 },
636 ext_port
=> { optional
=> 1 },
637 smarthost
=> { optional
=> 1 },
638 smarthostport
=> { optional
=> 1 },
639 relay
=> { optional
=> 1 },
640 relayport
=> { optional
=> 1 },
641 relaynomx
=> { optional
=> 1 },
642 dwarning
=> { optional
=> 1 },
643 max_smtpd_in
=> { optional
=> 1 },
644 max_smtpd_out
=> { optional
=> 1 },
645 greylist
=> { optional
=> 1 },
646 helotests
=> { optional
=> 1 },
647 tls
=> { optional
=> 1 },
648 tlslog
=> { optional
=> 1 },
649 tlsheader
=> { optional
=> 1 },
650 spf
=> { optional
=> 1 },
651 maxsize
=> { optional
=> 1 },
652 banner
=> { optional
=> 1 },
653 max_filters
=> { optional
=> 1 },
654 max_policy
=> { optional
=> 1 },
655 hide_received
=> { optional
=> 1 },
656 rejectunknown
=> { optional
=> 1 },
657 rejectunknownsender
=> { optional
=> 1 },
658 conn_count_limit
=> { optional
=> 1 },
659 conn_rate_limit
=> { optional
=> 1 },
660 message_rate_limit
=> { optional
=> 1 },
661 verifyreceivers
=> { optional
=> 1 },
662 dnsbl_sites
=> { optional
=> 1 },
663 dnsbl_threshold
=> { optional
=> 1 },
676 use PVE
::Tools
qw($IPV4RE $IPV6RE);
683 PMG
::Config
::Admin-
>register();
684 PMG
::Config
::Mail-
>register();
685 PMG
::Config
::SpamQuarantine-
>register();
686 PMG
::Config
::VirusQuarantine-
>register();
687 PMG
::Config
::Spam-
>register();
688 PMG
::Config
::ClamAV-
>register();
690 # initialize all plugins
691 PMG
::Config
::Base-
>init();
693 PVE
::JSONSchema
::register_format
(
694 'transport-domain', \
&pmg_verify_transport_domain
);
696 sub pmg_verify_transport_domain
{
697 my ($name, $noerr) = @_;
699 # like dns-name, but can contain leading dot
700 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
702 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
703 return undef if $noerr;
704 die "value does not look like a valid transport domain\n";
709 PVE
::JSONSchema
::register_format
(
710 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
712 sub pmg_verify_transport_domain_or_email
{
713 my ($name, $noerr) = @_;
715 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
718 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
722 # like dns-name, but can contain leading dot
723 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
724 return undef if $noerr;
725 die "value does not look like a valid transport domain or email address\n";
730 PVE
::JSONSchema
::register_format
(
731 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
733 sub pmg_verify_dnsbl_entry
{
734 my ($name, $noerr) = @_;
736 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
737 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
738 # we don't implement the ';' separated numbers in pattern, because this
739 # breaks at PVE::JSONSchema::split_list
740 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
742 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
743 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
744 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
745 return undef if $noerr;
746 die "value '$name' does not look like a valid dnsbl entry\n";
754 my $class = ref($type) || $type;
756 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
758 return bless $cfg, $class;
764 PVE
::INotify
::write_file
("pmg.conf", $self);
767 my $lockfile = "/var/lock/pmgconfig.lck";
770 my ($code, $errmsg) = @_;
772 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
774 $errmsg ?
die "$errmsg: $err" : die $err;
780 my ($self, $section, $key, $value) = @_;
782 my $pdata = PMG
::Config
::Base-
>private();
784 my $plugin = $pdata->{plugins
}->{$section};
785 die "no such section '$section'" if !$plugin;
787 if (defined($value)) {
788 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
789 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
790 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
792 if (defined($self->{ids
}->{$section})) {
793 delete $self->{ids
}->{$section}->{$key};
800 # get section value or default
802 my ($self, $section, $key, $nodefault) = @_;
804 my $pdata = PMG
::Config
::Base-
>private();
805 my $pdesc = $pdata->{propertyList
}->{$key};
806 die "no such property '$section/$key'\n"
807 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
808 defined($pdata->{options
}->{$section}->{$key}));
810 if (defined($self->{ids
}->{$section}) &&
811 defined(my $value = $self->{ids
}->{$section}->{$key})) {
815 return undef if $nodefault;
817 return $pdesc->{default};
820 # get a whole section with default value
822 my ($self, $section) = @_;
824 my $pdata = PMG
::Config
::Base-
>private();
825 return undef if !defined($pdata->{options
}->{$section});
829 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
831 my $pdesc = $pdata->{propertyList
}->{$key};
833 if (defined($self->{ids
}->{$section}) &&
834 defined(my $value = $self->{ids
}->{$section}->{$key})) {
835 $res->{$key} = $value;
838 $res->{$key} = $pdesc->{default};
844 # get a whole config with default values
848 my $pdata = PMG
::Config
::Base-
>private();
852 foreach my $type (keys %{$pdata->{plugins
}}) {
853 my $plugin = $pdata->{plugins
}->{$type};
854 $res->{$type} = $self->get_section($type);
861 my ($filename, $fh) = @_;
863 local $/ = undef; # slurp mode
865 my $raw = <$fh> if defined($fh);
867 return PMG
::Config
::Base-
>parse_config($filename, $raw);
871 my ($filename, $fh, $cfg) = @_;
873 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
875 PVE
::Tools
::safe_print
($filename, $fh, $raw);
878 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
881 undef, always_call_parser
=> 1);
883 # parsers/writers for other files
885 my $domainsfilename = "/etc/pmg/domains";
887 sub postmap_pmg_domains
{
888 PMG
::Utils
::run_postmap
($domainsfilename);
891 sub read_pmg_domains
{
892 my ($filename, $fh) = @_;
898 while (defined(my $line = <$fh>)) {
900 next if $line =~ m/^\s*$/;
901 if ($line =~ m/^#(.*)\s*$/) {
905 if ($line =~ m/^(\S+)\s.*$/) {
907 $domains->{$domain} = {
908 domain
=> $domain, comment
=> $comment };
911 warn "parse error in '$filename': $line\n";
920 sub write_pmg_domains
{
921 my ($filename, $fh, $domains) = @_;
923 foreach my $domain (sort keys %$domains) {
924 my $comment = $domains->{$domain}->{comment
};
925 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
926 if defined($comment) && $comment !~ m/^\s*$/;
928 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
932 PVE
::INotify
::register_file
('domains', $domainsfilename,
935 undef, always_call_parser
=> 1);
937 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
939 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
942 undef, always_call_parser
=> 1);
944 my $mynetworks_filename = "/etc/pmg/mynetworks";
946 sub read_pmg_mynetworks
{
947 my ($filename, $fh) = @_;
953 while (defined(my $line = <$fh>)) {
955 next if $line =~ m/^\s*$/;
956 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
957 my ($network, $prefix_size, $comment) = ($1, $2, $3);
958 my $cidr = "$network/${prefix_size}";
959 $mynetworks->{$cidr} = {
961 network_address
=> $network,
962 prefix_size
=> $prefix_size,
963 comment
=> $comment // '',
966 warn "parse error in '$filename': $line\n";
974 sub write_pmg_mynetworks
{
975 my ($filename, $fh, $mynetworks) = @_;
977 foreach my $cidr (sort keys %$mynetworks) {
978 my $data = $mynetworks->{$cidr};
979 my $comment = $data->{comment
} // '*';
980 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
984 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
985 \
&read_pmg_mynetworks
,
986 \
&write_pmg_mynetworks
,
987 undef, always_call_parser
=> 1);
989 PVE
::JSONSchema
::register_format
(
990 'tls-policy', \
&pmg_verify_tls_policy
);
992 # TODO: extend to parse attributes of the policy
993 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
994 sub pmg_verify_tls_policy
{
995 my ($policy, $noerr) = @_;
997 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
998 return undef if $noerr;
999 die "value '$policy' does not look like a valid tls policy\n";
1004 PVE
::JSONSchema
::register_format
(
1005 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1007 sub pmg_verify_tls_policy_strict
{
1008 my ($policy, $noerr) = @_;
1010 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1011 return undef if $noerr;
1012 die "value '$policy' does not look like a valid tls policy\n";
1017 sub read_tls_policy
{
1018 my ($filename, $fh) = @_;
1020 return {} if !defined($fh);
1022 my $tls_policy = {};
1024 while (defined(my $line = <$fh>)) {
1026 next if $line =~ m/^\s*$/;
1027 next if $line =~ m/^#(.*)\s*$/;
1029 my $parse_error = sub {
1031 die "parse error in '$filename': $line - $err";
1034 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1035 my ($domain, $policy) = ($1, $2);
1038 pmg_verify_transport_domain
($domain);
1039 pmg_verify_tls_policy
($policy);
1042 $parse_error->($err);
1046 $tls_policy->{$domain} = {
1051 $parse_error->('wrong format');
1058 sub write_tls_policy
{
1059 my ($filename, $fh, $tls_policy) = @_;
1061 return if !$tls_policy;
1063 foreach my $domain (sort keys %$tls_policy) {
1064 my $entry = $tls_policy->{$domain};
1065 PVE
::Tools
::safe_print
(
1066 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1070 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1071 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1074 undef, always_call_parser
=> 1);
1076 sub postmap_tls_policy
{
1077 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1080 my $transport_map_filename = "/etc/pmg/transport";
1082 sub postmap_pmg_transport
{
1083 PMG
::Utils
::run_postmap
($transport_map_filename);
1086 sub read_transport_map
{
1087 my ($filename, $fh) = @_;
1089 return [] if !defined($fh);
1095 while (defined(my $line = <$fh>)) {
1097 next if $line =~ m/^\s*$/;
1098 if ($line =~ m/^#(.*)\s*$/) {
1103 my $parse_error = sub {
1105 warn "parse error in '$filename': $line - $err";
1109 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1110 my ($domain, $host, $port) = ($1, $2, $3);
1112 eval { pmg_verify_transport_domain_or_email
($domain); };
1114 $parse_error->($err);
1118 if ($host =~ m/^\[(.*)\]$/) {
1123 eval { PVE
::JSONSchema
::pve_verify_address
($host); };
1125 $parse_error->($err);
1134 comment
=> $comment,
1136 $res->{$domain} = $data;
1139 $parse_error->('wrong format');
1146 sub write_transport_map
{
1147 my ($filename, $fh, $tmap) = @_;
1151 foreach my $domain (sort keys %$tmap) {
1152 my $data = $tmap->{$domain};
1154 my $comment = $data->{comment
};
1155 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1156 if defined($comment) && $comment !~ m/^\s*$/;
1158 my $use_mx = $data->{use_mx
};
1159 $use_mx = 0 if $data->{host
} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1162 PVE
::Tools
::safe_print
(
1163 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1165 PVE
::Tools
::safe_print
(
1166 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1171 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1172 \
&read_transport_map
,
1173 \
&write_transport_map
,
1174 undef, always_call_parser
=> 1);
1176 # config file generation using templates
1178 sub get_template_vars
{
1181 my $vars = { pmg
=> $self->get_config() };
1183 my $nodename = PVE
::INotify
::nodename
();
1184 my $int_ip = PMG
::Cluster
::remote_node_ip
($nodename);
1185 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1187 my $transportnets = [];
1189 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1190 foreach my $domain (sort keys %$tmap) {
1191 my $data = $tmap->{$domain};
1192 my $host = $data->{host
};
1193 if ($host =~ m/^$IPV4RE$/) {
1194 push @$transportnets, "$host/32";
1195 } elsif ($host =~ m/^$IPV6RE$/) {
1196 push @$transportnets, "[$host]/128";
1201 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1203 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1205 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1206 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1207 push @$mynetworks, "[$1]/$2";
1209 push @$mynetworks, $int_net_cidr;
1212 if ($int_ip =~ m/^$IPV6RE$/) {
1213 push @$mynetworks, "[$int_ip]/128";
1215 push @$mynetworks, "$int_ip/32";
1219 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1220 foreach my $cidr (sort keys %$netlist) {
1221 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1222 push @$mynetworks, "[$1]/$2";
1224 push @$mynetworks, $cidr;
1228 push @$mynetworks, @$transportnets;
1230 # add default relay to mynetworks
1231 if (my $relay = $self->get('mail', 'relay')) {
1232 if ($relay =~ m/^$IPV4RE$/) {
1233 push @$mynetworks, "$relay/32";
1234 } elsif ($relay =~ m/^$IPV6RE$/) {
1235 push @$mynetworks, "[$relay]/128";
1237 # DNS name - do nothing ?
1241 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1243 # normalize dnsbl_sites
1244 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1245 if (scalar(@dnsbl_sites)) {
1246 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1249 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1252 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1253 $self->get('mail', 'spf');
1254 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1256 if ($int_ip =~ m/^$IPV6RE$/) {
1257 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1259 $vars->{postfix
}->{int_ip
} = $int_ip;
1262 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1263 $vars->{dns
}->{hostname
} = $nodename;
1265 my $domain = $resolv->{search
} // 'localdomain';
1266 $vars->{dns
}->{domain
} = $domain;
1268 my $wlbr = "$nodename.$domain";
1269 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1272 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1274 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1276 my $uri = URI-
>new($proxy);
1277 my $host = $uri->host;
1278 my $port = $uri->port // 8080;
1280 my $data = { host
=> $host, port
=> $port };
1281 if (my $ui = $uri->userinfo) {
1282 my ($username, $pw) = split(/:/, $ui, 2);
1283 $data->{username
} = $username;
1284 $data->{password
} = $pw if defined($pw);
1286 $vars->{proxy
} = $data;
1289 warn "parse http_proxy failed - $@" if $@;
1291 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1296 # use one global TT cache
1297 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1299 my $template_toolkit;
1301 sub get_template_toolkit
{
1303 return $template_toolkit if $template_toolkit;
1305 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1307 return $template_toolkit;
1310 # rewrite file from template
1311 # return true if file has changed
1312 sub rewrite_config_file
{
1313 my ($self, $tmplname, $dstfn) = @_;
1315 my $demo = $self->get('admin', 'demo');
1318 my $demosrc = "$tmplname.demo";
1319 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1322 my ($perm, $uid, $gid);
1324 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1325 # needed if file contains a HTTPProxyPasswort
1327 $uid = getpwnam('clamav');
1328 $gid = getgrnam('adm');
1332 my $tt = get_template_toolkit
();
1334 my $vars = $self->get_template_vars();
1338 $tt->process($tmplname, $vars, \
$output) ||
1339 die $tt->error() . "\n";
1341 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1343 return 0 if defined($old) && ($old eq $output); # no change
1345 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1347 if (defined($uid) && defined($gid)) {
1348 chown($uid, $gid, $dstfn);
1354 # rewrite spam configuration
1355 sub rewrite_config_spam
{
1358 my $use_awl = $self->get('spam', 'use_awl');
1359 my $use_bayes = $self->get('spam', 'use_bayes');
1360 my $use_razor = $self->get('spam', 'use_razor');
1364 # delete AW and bayes databases if those features are disabled
1366 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1370 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1371 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1372 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1375 # make sure we have a custom.cf file (else cluster sync fails)
1376 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1378 $changes = 1 if $self->rewrite_config_file(
1379 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1381 $changes = 1 if $self->rewrite_config_file(
1382 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1384 $changes = 1 if $self->rewrite_config_file(
1385 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1387 $changes = 1 if $self->rewrite_config_file(
1388 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1391 mkdir "/root/.razor";
1393 $changes = 1 if $self->rewrite_config_file(
1394 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1396 if (! -e
'/root/.razor/identity') {
1399 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1400 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1403 syslog
('info', "registering razor failed: $err") if $err;
1410 # rewrite ClamAV configuration
1411 sub rewrite_config_clam
{
1414 return $self->rewrite_config_file(
1415 'clamd.conf.in', '/etc/clamav/clamd.conf');
1418 sub rewrite_config_freshclam
{
1421 return $self->rewrite_config_file(
1422 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1425 sub rewrite_config_postgres
{
1428 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1429 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1433 $changes = 1 if $self->rewrite_config_file(
1434 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1436 $changes = 1 if $self->rewrite_config_file(
1437 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1442 # rewrite /root/.forward
1443 sub rewrite_dot_forward
{
1446 my $dstfn = '/root/.forward';
1448 my $email = $self->get('admin', 'email');
1451 if ($email && $email =~ m/\s*(\S+)\s*/) {
1454 # empty .forward does not forward mails (see man local)
1457 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1459 return 0 if defined($old) && ($old eq $output); # no change
1461 PVE
::Tools
::file_set_contents
($dstfn, $output);
1466 my $write_smtp_whitelist = sub {
1467 my ($filename, $data, $action) = @_;
1469 $action = 'OK' if !$action;
1471 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1475 foreach my $k (sort keys %$data) {
1476 $new .= "$k $action\n";
1479 return 0 if defined($old) && ($old eq $new); # no change
1481 PVE
::Tools
::file_set_contents
($filename, $new);
1483 PMG
::Utils
::run_postmap
($filename);
1488 sub rewrite_postfix_whitelist
{
1489 my ($rulecache) = @_;
1491 # see man page for regexp_table for postfix regex table format
1493 # we use a hash to avoid duplicate entries in regex tables
1496 my $clientlist = {};
1498 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1499 my $oclass = ref($obj);
1500 if ($oclass eq 'PMG::RuleDB::Receiver') {
1501 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1502 $tolist->{"/^$addr\$/"} = 1;
1503 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1504 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1505 $tolist->{"/^.+\@$addr\$/"} = 1;
1506 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1507 my $addr = $obj->{address
};
1509 $tolist->{"/^$addr\$/"} = 1;
1513 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1514 my $oclass = ref($obj);
1515 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1516 if ($oclass eq 'PMG::RuleDB::EMail') {
1517 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1518 $fromlist->{"/^$addr\$/"} = 1;
1519 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1520 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1521 $fromlist->{"/^.+\@$addr\$/"} = 1;
1522 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1523 my $addr = $obj->{address
};
1525 $fromlist->{"/^$addr\$/"} = 1;
1526 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1527 $clientlist->{$obj->{address
}} = 1;
1528 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1529 $clientlist->{$obj->{address
}} = 1;
1533 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1534 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1535 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1536 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1539 # rewrite /etc/postfix/*
1540 sub rewrite_config_postfix
{
1541 my ($self, $rulecache) = @_;
1543 # make sure we have required files (else postfix start fails)
1544 IO
::File-
>new($transport_map_filename, 'a', 0644);
1548 if ($self->get('mail', 'tls')) {
1550 PMG
::Utils
::gen_proxmox_tls_cert
();
1552 syslog
('info', "generating certificate failed: $@") if $@;
1555 $changes = 1 if $self->rewrite_config_file(
1556 'main.cf.in', '/etc/postfix/main.cf');
1558 $changes = 1 if $self->rewrite_config_file(
1559 'master.cf.in', '/etc/postfix/master.cf');
1561 # make sure we have required files (else postfix start fails)
1562 # Note: postmap need a valid /etc/postfix/main.cf configuration
1563 postmap_pmg_domains
();
1564 postmap_pmg_transport
();
1565 postmap_tls_policy
();
1567 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1569 # make sure aliases.db is up to date
1570 system('/usr/bin/newaliases');
1575 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1576 my $pmg_service_params = {
1577 mail
=> { hide_received
=> 1 },
1581 dkim_sign_all_mail
=> 1,
1585 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1586 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1588 sub dump_smtp_filter_config
{
1593 foreach my $sec (sort keys %$pmg_service_params) {
1594 my $conf_sec = $self->{ids
}->{$sec} // {};
1595 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1596 $val = $conf_sec->{$key};
1597 $conf .= "$sec.$key:$val\n" if defined($val);
1604 sub compare_smtp_filter_config
{
1610 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1614 syslog
('warning', "reloading pmg-smtp-filter: $err");
1617 my $new = $self->dump_smtp_filter_config();
1618 $ret = 1 if $old ne $new;
1621 $self->write_smtp_filter_config() if $ret;
1626 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1628 sub write_smtp_filter_config
{
1631 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1632 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1633 $self->dump_smtp_filter_config());
1639 sub rewrite_config
{
1640 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1642 $force_restart = {} if ! $force_restart;
1644 my $log_restart = sub {
1645 syslog
('info', "configuration change detected for '$_[0]', restarting");
1648 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1649 $force_restart->{postfix
}) {
1650 $log_restart->('postfix');
1651 PMG
::Utils
::service_cmd
('postfix', 'reload');
1654 if ($self->rewrite_dot_forward() && $restart_services) {
1655 # no need to restart anything
1658 if ($self->rewrite_config_postgres() && $restart_services) {
1659 # do nothing (too many side effects)?
1660 # does not happen anyways, because config does not change.
1663 if (($self->rewrite_config_spam() && $restart_services) ||
1664 $force_restart->{spam
}) {
1665 $log_restart->('pmg-smtp-filter');
1666 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1669 if (($self->rewrite_config_clam() && $restart_services) ||
1670 $force_restart->{clam
}) {
1671 $log_restart->('clamav-daemon');
1672 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1675 if (($self->rewrite_config_freshclam() && $restart_services) ||
1676 $force_restart->{freshclam
}) {
1677 $log_restart->('clamav-freshclam');
1678 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1681 if (($self->compare_smtp_filter_config() && $restart_services) ||
1682 $force_restart->{spam
}) {
1683 syslog
('info', "scheduled reload for pmg-smtp-filter");
1684 PMG
::Utils
::reload_smtp_filter
();