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).",
630 before_queue_filtering
=> {
631 description
=> "Enable before queue filtering by pmg-smtp-filter",
636 description
=> "Send out NDR when mail gets blocked",
645 int_port
=> { optional
=> 1 },
646 ext_port
=> { optional
=> 1 },
647 smarthost
=> { optional
=> 1 },
648 smarthostport
=> { optional
=> 1 },
649 relay
=> { optional
=> 1 },
650 relayport
=> { optional
=> 1 },
651 relaynomx
=> { optional
=> 1 },
652 dwarning
=> { optional
=> 1 },
653 max_smtpd_in
=> { optional
=> 1 },
654 max_smtpd_out
=> { optional
=> 1 },
655 greylist
=> { optional
=> 1 },
656 helotests
=> { optional
=> 1 },
657 tls
=> { optional
=> 1 },
658 tlslog
=> { optional
=> 1 },
659 tlsheader
=> { optional
=> 1 },
660 spf
=> { optional
=> 1 },
661 maxsize
=> { optional
=> 1 },
662 banner
=> { optional
=> 1 },
663 max_filters
=> { optional
=> 1 },
664 max_policy
=> { optional
=> 1 },
665 hide_received
=> { optional
=> 1 },
666 rejectunknown
=> { optional
=> 1 },
667 rejectunknownsender
=> { optional
=> 1 },
668 conn_count_limit
=> { optional
=> 1 },
669 conn_rate_limit
=> { optional
=> 1 },
670 message_rate_limit
=> { optional
=> 1 },
671 verifyreceivers
=> { optional
=> 1 },
672 dnsbl_sites
=> { optional
=> 1 },
673 dnsbl_threshold
=> { optional
=> 1 },
674 before_queue_filtering
=> { optional
=> 1 },
675 ndr_on_block
=> { optional
=> 1 },
688 use PVE
::Tools
qw($IPV4RE $IPV6RE);
695 PMG
::Config
::Admin-
>register();
696 PMG
::Config
::Mail-
>register();
697 PMG
::Config
::SpamQuarantine-
>register();
698 PMG
::Config
::VirusQuarantine-
>register();
699 PMG
::Config
::Spam-
>register();
700 PMG
::Config
::ClamAV-
>register();
702 # initialize all plugins
703 PMG
::Config
::Base-
>init();
705 PVE
::JSONSchema
::register_format
(
706 'transport-domain', \
&pmg_verify_transport_domain
);
708 sub pmg_verify_transport_domain
{
709 my ($name, $noerr) = @_;
711 # like dns-name, but can contain leading dot
712 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
714 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
715 return undef if $noerr;
716 die "value does not look like a valid transport domain\n";
721 PVE
::JSONSchema
::register_format
(
722 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
724 sub pmg_verify_transport_domain_or_email
{
725 my ($name, $noerr) = @_;
727 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
730 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
734 # like dns-name, but can contain leading dot
735 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
736 return undef if $noerr;
737 die "value does not look like a valid transport domain or email address\n";
742 PVE
::JSONSchema
::register_format
(
743 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
745 sub pmg_verify_dnsbl_entry
{
746 my ($name, $noerr) = @_;
748 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
749 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
750 # we don't implement the ';' separated numbers in pattern, because this
751 # breaks at PVE::JSONSchema::split_list
752 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
754 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
755 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
756 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
757 return undef if $noerr;
758 die "value '$name' does not look like a valid dnsbl entry\n";
766 my $class = ref($type) || $type;
768 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
770 return bless $cfg, $class;
776 PVE
::INotify
::write_file
("pmg.conf", $self);
779 my $lockfile = "/var/lock/pmgconfig.lck";
782 my ($code, $errmsg) = @_;
784 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
786 $errmsg ?
die "$errmsg: $err" : die $err;
792 my ($self, $section, $key, $value) = @_;
794 my $pdata = PMG
::Config
::Base-
>private();
796 my $plugin = $pdata->{plugins
}->{$section};
797 die "no such section '$section'" if !$plugin;
799 if (defined($value)) {
800 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
801 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
802 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
804 if (defined($self->{ids
}->{$section})) {
805 delete $self->{ids
}->{$section}->{$key};
812 # get section value or default
814 my ($self, $section, $key, $nodefault) = @_;
816 my $pdata = PMG
::Config
::Base-
>private();
817 my $pdesc = $pdata->{propertyList
}->{$key};
818 die "no such property '$section/$key'\n"
819 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
820 defined($pdata->{options
}->{$section}->{$key}));
822 if (defined($self->{ids
}->{$section}) &&
823 defined(my $value = $self->{ids
}->{$section}->{$key})) {
827 return undef if $nodefault;
829 return $pdesc->{default};
832 # get a whole section with default value
834 my ($self, $section) = @_;
836 my $pdata = PMG
::Config
::Base-
>private();
837 return undef if !defined($pdata->{options
}->{$section});
841 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
843 my $pdesc = $pdata->{propertyList
}->{$key};
845 if (defined($self->{ids
}->{$section}) &&
846 defined(my $value = $self->{ids
}->{$section}->{$key})) {
847 $res->{$key} = $value;
850 $res->{$key} = $pdesc->{default};
856 # get a whole config with default values
860 my $pdata = PMG
::Config
::Base-
>private();
864 foreach my $type (keys %{$pdata->{plugins
}}) {
865 my $plugin = $pdata->{plugins
}->{$type};
866 $res->{$type} = $self->get_section($type);
873 my ($filename, $fh) = @_;
875 local $/ = undef; # slurp mode
877 my $raw = <$fh> if defined($fh);
879 return PMG
::Config
::Base-
>parse_config($filename, $raw);
883 my ($filename, $fh, $cfg) = @_;
885 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
887 PVE
::Tools
::safe_print
($filename, $fh, $raw);
890 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
893 undef, always_call_parser
=> 1);
895 # parsers/writers for other files
897 my $domainsfilename = "/etc/pmg/domains";
899 sub postmap_pmg_domains
{
900 PMG
::Utils
::run_postmap
($domainsfilename);
903 sub read_pmg_domains
{
904 my ($filename, $fh) = @_;
910 while (defined(my $line = <$fh>)) {
912 next if $line =~ m/^\s*$/;
913 if ($line =~ m/^#(.*)\s*$/) {
917 if ($line =~ m/^(\S+)\s.*$/) {
919 $domains->{$domain} = {
920 domain
=> $domain, comment
=> $comment };
923 warn "parse error in '$filename': $line\n";
932 sub write_pmg_domains
{
933 my ($filename, $fh, $domains) = @_;
935 foreach my $domain (sort keys %$domains) {
936 my $comment = $domains->{$domain}->{comment
};
937 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
938 if defined($comment) && $comment !~ m/^\s*$/;
940 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
944 PVE
::INotify
::register_file
('domains', $domainsfilename,
947 undef, always_call_parser
=> 1);
949 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
951 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
954 undef, always_call_parser
=> 1);
956 my $mynetworks_filename = "/etc/pmg/mynetworks";
958 sub read_pmg_mynetworks
{
959 my ($filename, $fh) = @_;
965 while (defined(my $line = <$fh>)) {
967 next if $line =~ m/^\s*$/;
968 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
969 my ($network, $prefix_size, $comment) = ($1, $2, $3);
970 my $cidr = "$network/${prefix_size}";
971 $mynetworks->{$cidr} = {
973 network_address
=> $network,
974 prefix_size
=> $prefix_size,
975 comment
=> $comment // '',
978 warn "parse error in '$filename': $line\n";
986 sub write_pmg_mynetworks
{
987 my ($filename, $fh, $mynetworks) = @_;
989 foreach my $cidr (sort keys %$mynetworks) {
990 my $data = $mynetworks->{$cidr};
991 my $comment = $data->{comment
} // '*';
992 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
996 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
997 \
&read_pmg_mynetworks
,
998 \
&write_pmg_mynetworks
,
999 undef, always_call_parser
=> 1);
1001 PVE
::JSONSchema
::register_format
(
1002 'tls-policy', \
&pmg_verify_tls_policy
);
1004 # TODO: extend to parse attributes of the policy
1005 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1006 sub pmg_verify_tls_policy
{
1007 my ($policy, $noerr) = @_;
1009 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1010 return undef if $noerr;
1011 die "value '$policy' does not look like a valid tls policy\n";
1016 PVE
::JSONSchema
::register_format
(
1017 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1019 sub pmg_verify_tls_policy_strict
{
1020 my ($policy, $noerr) = @_;
1022 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1023 return undef if $noerr;
1024 die "value '$policy' does not look like a valid tls policy\n";
1029 sub read_tls_policy
{
1030 my ($filename, $fh) = @_;
1032 return {} if !defined($fh);
1034 my $tls_policy = {};
1036 while (defined(my $line = <$fh>)) {
1038 next if $line =~ m/^\s*$/;
1039 next if $line =~ m/^#(.*)\s*$/;
1041 my $parse_error = sub {
1043 die "parse error in '$filename': $line - $err";
1046 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1047 my ($domain, $policy) = ($1, $2);
1050 pmg_verify_transport_domain
($domain);
1051 pmg_verify_tls_policy
($policy);
1054 $parse_error->($err);
1058 $tls_policy->{$domain} = {
1063 $parse_error->('wrong format');
1070 sub write_tls_policy
{
1071 my ($filename, $fh, $tls_policy) = @_;
1073 return if !$tls_policy;
1075 foreach my $domain (sort keys %$tls_policy) {
1076 my $entry = $tls_policy->{$domain};
1077 PVE
::Tools
::safe_print
(
1078 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1082 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1083 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1086 undef, always_call_parser
=> 1);
1088 sub postmap_tls_policy
{
1089 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1092 my $transport_map_filename = "/etc/pmg/transport";
1094 sub postmap_pmg_transport
{
1095 PMG
::Utils
::run_postmap
($transport_map_filename);
1098 sub read_transport_map
{
1099 my ($filename, $fh) = @_;
1101 return [] if !defined($fh);
1107 while (defined(my $line = <$fh>)) {
1109 next if $line =~ m/^\s*$/;
1110 if ($line =~ m/^#(.*)\s*$/) {
1115 my $parse_error = sub {
1117 warn "parse error in '$filename': $line - $err";
1121 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1122 my ($domain, $host, $port) = ($1, $2, $3);
1124 eval { pmg_verify_transport_domain_or_email
($domain); };
1126 $parse_error->($err);
1130 if ($host =~ m/^\[(.*)\]$/) {
1135 eval { PVE
::JSONSchema
::pve_verify_address
($host); };
1137 $parse_error->($err);
1146 comment
=> $comment,
1148 $res->{$domain} = $data;
1151 $parse_error->('wrong format');
1158 sub write_transport_map
{
1159 my ($filename, $fh, $tmap) = @_;
1163 foreach my $domain (sort keys %$tmap) {
1164 my $data = $tmap->{$domain};
1166 my $comment = $data->{comment
};
1167 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1168 if defined($comment) && $comment !~ m/^\s*$/;
1170 my $use_mx = $data->{use_mx
};
1171 $use_mx = 0 if $data->{host
} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1174 PVE
::Tools
::safe_print
(
1175 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1177 PVE
::Tools
::safe_print
(
1178 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1183 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1184 \
&read_transport_map
,
1185 \
&write_transport_map
,
1186 undef, always_call_parser
=> 1);
1188 # config file generation using templates
1190 sub get_host_dns_info
{
1194 my $nodename = PVE
::INotify
::nodename
();
1196 $dnsinfo->{hostname
} = $nodename;
1197 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1199 my $domain = $resolv->{search
} // 'localdomain';
1200 $dnsinfo->{domain
} = $domain;
1202 $dnsinfo->{fqdn
} = "$nodename.$domain";
1207 sub get_template_vars
{
1210 my $vars = { pmg
=> $self->get_config() };
1212 my $dnsinfo = get_host_dns_info
();
1213 $vars->{dns
} = $dnsinfo;
1214 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1215 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1217 my $transportnets = [];
1219 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1220 foreach my $domain (sort keys %$tmap) {
1221 my $data = $tmap->{$domain};
1222 my $host = $data->{host
};
1223 if ($host =~ m/^$IPV4RE$/) {
1224 push @$transportnets, "$host/32";
1225 } elsif ($host =~ m/^$IPV6RE$/) {
1226 push @$transportnets, "[$host]/128";
1231 $vars->{postfix
}->{transportnets
} = join(' ', @$transportnets);
1233 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1235 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1236 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1237 push @$mynetworks, "[$1]/$2";
1239 push @$mynetworks, $int_net_cidr;
1242 if ($int_ip =~ m/^$IPV6RE$/) {
1243 push @$mynetworks, "[$int_ip]/128";
1245 push @$mynetworks, "$int_ip/32";
1249 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1250 foreach my $cidr (sort keys %$netlist) {
1251 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1252 push @$mynetworks, "[$1]/$2";
1254 push @$mynetworks, $cidr;
1258 push @$mynetworks, @$transportnets;
1260 # add default relay to mynetworks
1261 if (my $relay = $self->get('mail', 'relay')) {
1262 if ($relay =~ m/^$IPV4RE$/) {
1263 push @$mynetworks, "$relay/32";
1264 } elsif ($relay =~ m/^$IPV6RE$/) {
1265 push @$mynetworks, "[$relay]/128";
1267 # DNS name - do nothing ?
1271 $vars->{postfix
}->{mynetworks
} = join(' ', @$mynetworks);
1273 # normalize dnsbl_sites
1274 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1275 if (scalar(@dnsbl_sites)) {
1276 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1279 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1282 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1283 $self->get('mail', 'spf');
1284 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1286 if ($int_ip =~ m/^$IPV6RE$/) {
1287 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1289 $vars->{postfix
}->{int_ip
} = $int_ip;
1292 my $wlbr = $dnsinfo->{fqdn
};
1293 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1296 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1298 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1300 my $uri = URI-
>new($proxy);
1301 my $host = $uri->host;
1302 my $port = $uri->port // 8080;
1304 my $data = { host
=> $host, port
=> $port };
1305 if (my $ui = $uri->userinfo) {
1306 my ($username, $pw) = split(/:/, $ui, 2);
1307 $data->{username
} = $username;
1308 $data->{password
} = $pw if defined($pw);
1310 $vars->{proxy
} = $data;
1313 warn "parse http_proxy failed - $@" if $@;
1315 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1320 # use one global TT cache
1321 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1323 my $template_toolkit;
1325 sub get_template_toolkit
{
1327 return $template_toolkit if $template_toolkit;
1329 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1331 return $template_toolkit;
1334 # rewrite file from template
1335 # return true if file has changed
1336 sub rewrite_config_file
{
1337 my ($self, $tmplname, $dstfn) = @_;
1339 my $demo = $self->get('admin', 'demo');
1342 my $demosrc = "$tmplname.demo";
1343 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1346 my ($perm, $uid, $gid);
1348 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1349 # needed if file contains a HTTPProxyPasswort
1351 $uid = getpwnam('clamav');
1352 $gid = getgrnam('adm');
1356 my $tt = get_template_toolkit
();
1358 my $vars = $self->get_template_vars();
1362 $tt->process($tmplname, $vars, \
$output) ||
1363 die $tt->error() . "\n";
1365 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1367 return 0 if defined($old) && ($old eq $output); # no change
1369 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1371 if (defined($uid) && defined($gid)) {
1372 chown($uid, $gid, $dstfn);
1378 # rewrite spam configuration
1379 sub rewrite_config_spam
{
1382 my $use_awl = $self->get('spam', 'use_awl');
1383 my $use_bayes = $self->get('spam', 'use_bayes');
1384 my $use_razor = $self->get('spam', 'use_razor');
1388 # delete AW and bayes databases if those features are disabled
1390 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1394 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1395 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1396 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1399 # make sure we have a custom.cf file (else cluster sync fails)
1400 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1402 $changes = 1 if $self->rewrite_config_file(
1403 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1405 $changes = 1 if $self->rewrite_config_file(
1406 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1408 $changes = 1 if $self->rewrite_config_file(
1409 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1411 $changes = 1 if $self->rewrite_config_file(
1412 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1415 mkdir "/root/.razor";
1417 $changes = 1 if $self->rewrite_config_file(
1418 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1420 if (! -e
'/root/.razor/identity') {
1423 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1424 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1427 syslog
('info', "registering razor failed: $err") if $err;
1434 # rewrite ClamAV configuration
1435 sub rewrite_config_clam
{
1438 return $self->rewrite_config_file(
1439 'clamd.conf.in', '/etc/clamav/clamd.conf');
1442 sub rewrite_config_freshclam
{
1445 return $self->rewrite_config_file(
1446 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1449 sub rewrite_config_postgres
{
1452 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1453 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1457 $changes = 1 if $self->rewrite_config_file(
1458 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1460 $changes = 1 if $self->rewrite_config_file(
1461 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1466 # rewrite /root/.forward
1467 sub rewrite_dot_forward
{
1470 my $dstfn = '/root/.forward';
1472 my $email = $self->get('admin', 'email');
1475 if ($email && $email =~ m/\s*(\S+)\s*/) {
1478 # empty .forward does not forward mails (see man local)
1481 my $old = PVE
::Tools
::file_get_contents
($dstfn, 128*1024) if -f
$dstfn;
1483 return 0 if defined($old) && ($old eq $output); # no change
1485 PVE
::Tools
::file_set_contents
($dstfn, $output);
1490 my $write_smtp_whitelist = sub {
1491 my ($filename, $data, $action) = @_;
1493 $action = 'OK' if !$action;
1495 my $old = PVE
::Tools
::file_get_contents
($filename, 1024*1024)
1499 foreach my $k (sort keys %$data) {
1500 $new .= "$k $action\n";
1503 return 0 if defined($old) && ($old eq $new); # no change
1505 PVE
::Tools
::file_set_contents
($filename, $new);
1507 PMG
::Utils
::run_postmap
($filename);
1512 sub rewrite_postfix_whitelist
{
1513 my ($rulecache) = @_;
1515 # see man page for regexp_table for postfix regex table format
1517 # we use a hash to avoid duplicate entries in regex tables
1520 my $clientlist = {};
1522 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1523 my $oclass = ref($obj);
1524 if ($oclass eq 'PMG::RuleDB::Receiver') {
1525 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1526 $tolist->{"/^$addr\$/"} = 1;
1527 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1528 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1529 $tolist->{"/^.+\@$addr\$/"} = 1;
1530 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1531 my $addr = $obj->{address
};
1533 $tolist->{"/^$addr\$/"} = 1;
1537 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1538 my $oclass = ref($obj);
1539 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1540 if ($oclass eq 'PMG::RuleDB::EMail') {
1541 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1542 $fromlist->{"/^$addr\$/"} = 1;
1543 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1544 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1545 $fromlist->{"/^.+\@$addr\$/"} = 1;
1546 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1547 my $addr = $obj->{address
};
1549 $fromlist->{"/^$addr\$/"} = 1;
1550 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1551 $clientlist->{$obj->{address
}} = 1;
1552 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1553 $clientlist->{$obj->{address
}} = 1;
1557 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1558 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1559 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1560 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1563 # rewrite /etc/postfix/*
1564 sub rewrite_config_postfix
{
1565 my ($self, $rulecache) = @_;
1567 # make sure we have required files (else postfix start fails)
1568 IO
::File-
>new($transport_map_filename, 'a', 0644);
1572 if ($self->get('mail', 'tls')) {
1574 PMG
::Utils
::gen_proxmox_tls_cert
();
1576 syslog
('info', "generating certificate failed: $@") if $@;
1579 $changes = 1 if $self->rewrite_config_file(
1580 'main.cf.in', '/etc/postfix/main.cf');
1582 $changes = 1 if $self->rewrite_config_file(
1583 'master.cf.in', '/etc/postfix/master.cf');
1585 # make sure we have required files (else postfix start fails)
1586 # Note: postmap need a valid /etc/postfix/main.cf configuration
1587 postmap_pmg_domains
();
1588 postmap_pmg_transport
();
1589 postmap_tls_policy
();
1591 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1593 # make sure aliases.db is up to date
1594 system('/usr/bin/newaliases');
1599 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1600 my $pmg_service_params = {
1601 mail
=> { hide_received
=> 1 },
1605 dkim_sign_all_mail
=> 1,
1609 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1610 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1612 sub dump_smtp_filter_config
{
1617 foreach my $sec (sort keys %$pmg_service_params) {
1618 my $conf_sec = $self->{ids
}->{$sec} // {};
1619 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1620 $val = $conf_sec->{$key};
1621 $conf .= "$sec.$key:$val\n" if defined($val);
1628 sub compare_smtp_filter_config
{
1634 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1638 syslog
('warning', "reloading pmg-smtp-filter: $err");
1641 my $new = $self->dump_smtp_filter_config();
1642 $ret = 1 if $old ne $new;
1645 $self->write_smtp_filter_config() if $ret;
1650 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1652 sub write_smtp_filter_config
{
1655 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1656 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1657 $self->dump_smtp_filter_config());
1663 sub rewrite_config
{
1664 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1666 $force_restart = {} if ! $force_restart;
1668 my $log_restart = sub {
1669 syslog
('info', "configuration change detected for '$_[0]', restarting");
1672 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1673 $force_restart->{postfix
}) {
1674 $log_restart->('postfix');
1675 PMG
::Utils
::service_cmd
('postfix', 'reload');
1678 if ($self->rewrite_dot_forward() && $restart_services) {
1679 # no need to restart anything
1682 if ($self->rewrite_config_postgres() && $restart_services) {
1683 # do nothing (too many side effects)?
1684 # does not happen anyways, because config does not change.
1687 if (($self->rewrite_config_spam() && $restart_services) ||
1688 $force_restart->{spam
}) {
1689 $log_restart->('pmg-smtp-filter');
1690 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1693 if (($self->rewrite_config_clam() && $restart_services) ||
1694 $force_restart->{clam
}) {
1695 $log_restart->('clamav-daemon');
1696 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1699 if (($self->rewrite_config_freshclam() && $restart_services) ||
1700 $force_restart->{freshclam
}) {
1701 $log_restart->('clamav-freshclam');
1702 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1705 if (($self->compare_smtp_filter_config() && $restart_services) ||
1706 $force_restart->{spam
}) {
1707 syslog
('info', "scheduled reload for pmg-smtp-filter");
1708 PMG
::Utils
::reload_smtp_filter
();