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, 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.",
293 description
=> "Enables user self-service for Quarantine Links. Caution: this is accessible without authentication",
302 mailfrom
=> { optional
=> 1 },
303 hostname
=> { optional
=> 1 },
304 lifetime
=> { optional
=> 1 },
305 authmode
=> { optional
=> 1 },
306 reportstyle
=> { optional
=> 1 },
307 viewimages
=> { optional
=> 1 },
308 allowhrefs
=> { optional
=> 1 },
309 port
=> { optional
=> 1 },
310 protocol
=> { optional
=> 1 },
311 quarantinelink
=> { optional
=> 1 },
315 package PMG
::Config
::VirusQuarantine
;
320 use base
qw(PMG::Config::Base);
332 lifetime
=> { optional
=> 1 },
333 viewimages
=> { optional
=> 1 },
334 allowhrefs
=> { optional
=> 1 },
338 package PMG
::Config
::ClamAV
;
343 use base
qw(PMG::Config::Base);
352 description
=> "ClamAV database mirror server.",
354 default => 'database.clamav.net',
356 archiveblockencrypted
=> {
357 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'.",
362 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.",
368 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.",
374 description
=> "Files larger than this limit (in bytes) won't be scanned.",
380 description
=> "Sets the maximum amount of data (in bytes) to be scanned for each input file.",
383 default => 100000000,
386 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
391 # FIXME: remove for PMG 8.0 - https://blog.clamav.net/2021/04/are-you-still-attempting-to-download.html
393 description
=> "Enables support for Google Safe Browsing. (deprecated option, will be ignored)",
398 description
=> "Enables ScriptedUpdates (incremental download of signatures)",
407 archiveblockencrypted
=> { optional
=> 1 },
408 archivemaxrec
=> { optional
=> 1 },
409 archivemaxfiles
=> { optional
=> 1 },
410 archivemaxsize
=> { optional
=> 1 },
411 maxscansize
=> { optional
=> 1 },
412 dbmirror
=> { optional
=> 1 },
413 maxcccount
=> { optional
=> 1 },
414 safebrowsing
=> { optional
=> 1 }, # FIXME: remove for PMG 8.0
415 scriptedupdates
=> { optional
=> 1},
419 package PMG
::Config
::Mail
;
424 use PVE
::ProcFSTools
;
426 use base
qw(PMG::Config::Base);
433 sub physical_memory
{
435 return $physicalmem if $physicalmem;
437 my $info = PVE
::ProcFSTools
::read_meminfo
();
438 my $total = int($info->{memtotal
} / (1024*1024));
443 sub get_max_filters
{
444 # estimate optimal number of filter servers
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 = 40 if $max_servers > 40;
453 return $max_servers - 2;
457 # estimate optimal number of smtpd daemons
459 my $max_servers = 25;
461 my $memory = physical_memory
();
462 my $add_servers = int(($memory - 512)/$servermem);
463 $max_servers += $add_servers if $add_servers > 0;
464 $max_servers = 100 if $max_servers > 100;
469 # estimate optimal number of proxpolicy servers
471 my $memory = physical_memory
();
472 $max_servers = 5 if $memory >= 500;
479 description
=> "SMTP port number for outgoing mail (trusted).",
486 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
493 description
=> "The default mail delivery transport (incoming mails).",
494 type
=> 'string', format
=> 'address',
497 description
=> "Transport protocol for relay host.",
499 enum
=> [qw(smtp lmtp)],
503 description
=> "SMTP/LMTP port number for relay host.",
510 description
=> "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
515 description
=> "When set, all outgoing mails are deliverd to the specified smarthost.",
516 type
=> 'string', format
=> 'address',
519 description
=> "SMTP port number for smarthost.",
526 description
=> "ESMTP banner.",
529 default => 'ESMTP Proxmox',
532 description
=> "Maximum number of pmg-smtp-filter processes.",
536 default => get_max_filters
(),
539 description
=> "Maximum number of pmgpolicy processes.",
543 default => get_max_policy
(),
546 description
=> "Maximum number of SMTP daemon processes (in).",
550 default => get_max_smtpd
(),
553 description
=> "Maximum number of SMTP daemon processes (out).",
557 default => get_max_smtpd
(),
559 conn_count_limit
=> {
560 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
566 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.",
571 message_rate_limit
=> {
572 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.",
578 description
=> "Hide received header in outgoing mails.",
583 description
=> "Maximum email size. Larger mails are rejected.",
586 default => 1024*1024*10,
589 description
=> "SMTP delay warning time (in hours).",
595 description
=> "Enable TLS.",
600 description
=> "Enable TLS Logging.",
605 description
=> "Add TLS received header.",
610 description
=> "Use Sender Policy Framework.",
615 description
=> "Use Greylisting for IPv4.",
620 description
=> "Netmask to apply for greylisting IPv4 hosts",
627 description
=> "Use Greylisting for IPv6.",
632 description
=> "Netmask to apply for greylisting IPv6 hosts",
639 description
=> "Use SMTP HELO tests.",
644 description
=> "Reject unknown clients.",
648 rejectunknownsender
=> {
649 description
=> "Reject unknown senders.",
654 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
656 enum
=> ['450', '550'],
659 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
660 type
=> 'string', format
=> 'dnsbl-entry-list',
663 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
668 before_queue_filtering
=> {
669 description
=> "Enable before queue filtering by pmg-smtp-filter",
674 description
=> "Send out NDR when mail gets blocked",
683 int_port
=> { optional
=> 1 },
684 ext_port
=> { optional
=> 1 },
685 smarthost
=> { optional
=> 1 },
686 smarthostport
=> { optional
=> 1 },
687 relay
=> { optional
=> 1 },
688 relayprotocol
=> { optional
=> 1 },
689 relayport
=> { optional
=> 1 },
690 relaynomx
=> { optional
=> 1 },
691 dwarning
=> { optional
=> 1 },
692 max_smtpd_in
=> { optional
=> 1 },
693 max_smtpd_out
=> { optional
=> 1 },
694 greylist
=> { optional
=> 1 },
695 greylistmask4
=> { optional
=> 1 },
696 greylist6
=> { optional
=> 1 },
697 greylistmask6
=> { optional
=> 1 },
698 helotests
=> { optional
=> 1 },
699 tls
=> { optional
=> 1 },
700 tlslog
=> { optional
=> 1 },
701 tlsheader
=> { optional
=> 1 },
702 spf
=> { optional
=> 1 },
703 maxsize
=> { optional
=> 1 },
704 banner
=> { optional
=> 1 },
705 max_filters
=> { optional
=> 1 },
706 max_policy
=> { optional
=> 1 },
707 hide_received
=> { optional
=> 1 },
708 rejectunknown
=> { optional
=> 1 },
709 rejectunknownsender
=> { optional
=> 1 },
710 conn_count_limit
=> { optional
=> 1 },
711 conn_rate_limit
=> { optional
=> 1 },
712 message_rate_limit
=> { optional
=> 1 },
713 verifyreceivers
=> { optional
=> 1 },
714 dnsbl_sites
=> { optional
=> 1 },
715 dnsbl_threshold
=> { optional
=> 1 },
716 before_queue_filtering
=> { optional
=> 1 },
717 ndr_on_block
=> { optional
=> 1 },
730 use PVE
::Tools
qw($IPV4RE $IPV6RE);
737 PMG
::Config
::Admin-
>register();
738 PMG
::Config
::Mail-
>register();
739 PMG
::Config
::SpamQuarantine-
>register();
740 PMG
::Config
::VirusQuarantine-
>register();
741 PMG
::Config
::Spam-
>register();
742 PMG
::Config
::ClamAV-
>register();
744 # initialize all plugins
745 PMG
::Config
::Base-
>init();
747 PVE
::JSONSchema
::register_format
(
748 'transport-domain', \
&pmg_verify_transport_domain
);
750 sub pmg_verify_transport_domain
{
751 my ($name, $noerr) = @_;
753 # like dns-name, but can contain leading dot
754 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
756 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
757 return undef if $noerr;
758 die "value does not look like a valid transport domain\n";
763 PVE
::JSONSchema
::register_format
(
764 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
766 sub pmg_verify_transport_domain_or_email
{
767 my ($name, $noerr) = @_;
769 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
772 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
776 # like dns-name, but can contain leading dot
777 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
778 return undef if $noerr;
779 die "value does not look like a valid transport domain or email address\n";
784 PVE
::JSONSchema
::register_format
(
785 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
787 sub pmg_verify_dnsbl_entry
{
788 my ($name, $noerr) = @_;
790 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
791 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
792 # we don't implement the ';' separated numbers in pattern, because this
793 # breaks at PVE::JSONSchema::split_list
794 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
796 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
797 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
798 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
799 return undef if $noerr;
800 die "value '$name' does not look like a valid dnsbl entry\n";
808 my $class = ref($type) || $type;
810 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
812 return bless $cfg, $class;
818 PVE
::INotify
::write_file
("pmg.conf", $self);
821 my $lockfile = "/var/lock/pmgconfig.lck";
824 my ($code, $errmsg) = @_;
826 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
828 $errmsg ?
die "$errmsg: $err" : die $err;
834 my ($self, $section, $key, $value) = @_;
836 my $pdata = PMG
::Config
::Base-
>private();
838 my $plugin = $pdata->{plugins
}->{$section};
839 die "no such section '$section'" if !$plugin;
841 if (defined($value)) {
842 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
843 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
844 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
846 if (defined($self->{ids
}->{$section})) {
847 delete $self->{ids
}->{$section}->{$key};
854 # get section value or default
856 my ($self, $section, $key, $nodefault) = @_;
858 my $pdata = PMG
::Config
::Base-
>private();
859 my $pdesc = $pdata->{propertyList
}->{$key};
860 die "no such property '$section/$key'\n"
861 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
862 defined($pdata->{options
}->{$section}->{$key}));
864 if (defined($self->{ids
}->{$section}) &&
865 defined(my $value = $self->{ids
}->{$section}->{$key})) {
869 return undef if $nodefault;
871 return $pdesc->{default};
874 # get a whole section with default value
876 my ($self, $section) = @_;
878 my $pdata = PMG
::Config
::Base-
>private();
879 return undef if !defined($pdata->{options
}->{$section});
883 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
885 my $pdesc = $pdata->{propertyList
}->{$key};
887 if (defined($self->{ids
}->{$section}) &&
888 defined(my $value = $self->{ids
}->{$section}->{$key})) {
889 $res->{$key} = $value;
892 $res->{$key} = $pdesc->{default};
898 # get a whole config with default values
902 my $pdata = PMG
::Config
::Base-
>private();
906 foreach my $type (keys %{$pdata->{plugins
}}) {
907 my $plugin = $pdata->{plugins
}->{$type};
908 $res->{$type} = $self->get_section($type);
915 my ($filename, $fh) = @_;
917 local $/ = undef; # slurp mode
920 $raw = <$fh> if defined($fh);
922 return PMG
::Config
::Base-
>parse_config($filename, $raw);
926 my ($filename, $fh, $cfg) = @_;
928 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
930 PVE
::Tools
::safe_print
($filename, $fh, $raw);
933 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
936 undef, always_call_parser
=> 1);
938 # parsers/writers for other files
940 my $domainsfilename = "/etc/pmg/domains";
942 sub postmap_pmg_domains
{
943 PMG
::Utils
::run_postmap
($domainsfilename);
946 sub read_pmg_domains
{
947 my ($filename, $fh) = @_;
953 while (defined(my $line = <$fh>)) {
955 next if $line =~ m/^\s*$/;
956 if ($line =~ m/^#(.*)\s*$/) {
960 if ($line =~ m/^(\S+)\s.*$/) {
962 $domains->{$domain} = {
963 domain
=> $domain, comment
=> $comment };
966 warn "parse error in '$filename': $line\n";
975 sub write_pmg_domains
{
976 my ($filename, $fh, $domains) = @_;
978 foreach my $domain (sort keys %$domains) {
979 my $comment = $domains->{$domain}->{comment
};
980 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
981 if defined($comment) && $comment !~ m/^\s*$/;
983 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
987 PVE
::INotify
::register_file
('domains', $domainsfilename,
990 undef, always_call_parser
=> 1);
992 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
994 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
997 undef, always_call_parser
=> 1);
999 my $mynetworks_filename = "/etc/pmg/mynetworks";
1001 sub read_pmg_mynetworks
{
1002 my ($filename, $fh) = @_;
1004 my $mynetworks = {};
1008 while (defined(my $line = <$fh>)) {
1010 next if $line =~ m/^\s*$/;
1011 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1012 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1013 my $cidr = "$network/${prefix_size}";
1014 $mynetworks->{$cidr} = {
1016 network_address
=> $network,
1017 prefix_size
=> $prefix_size,
1018 comment
=> $comment // '',
1021 warn "parse error in '$filename': $line\n";
1029 sub write_pmg_mynetworks
{
1030 my ($filename, $fh, $mynetworks) = @_;
1032 foreach my $cidr (sort keys %$mynetworks) {
1033 my $data = $mynetworks->{$cidr};
1034 my $comment = $data->{comment
} // '*';
1035 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1039 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1040 \
&read_pmg_mynetworks
,
1041 \
&write_pmg_mynetworks
,
1042 undef, always_call_parser
=> 1);
1044 PVE
::JSONSchema
::register_format
(
1045 'tls-policy', \
&pmg_verify_tls_policy
);
1047 # TODO: extend to parse attributes of the policy
1048 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1049 sub pmg_verify_tls_policy
{
1050 my ($policy, $noerr) = @_;
1052 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1053 return undef if $noerr;
1054 die "value '$policy' does not look like a valid tls policy\n";
1059 PVE
::JSONSchema
::register_format
(
1060 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1062 sub pmg_verify_tls_policy_strict
{
1063 my ($policy, $noerr) = @_;
1065 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1066 return undef if $noerr;
1067 die "value '$policy' does not look like a valid tls policy\n";
1072 PVE
::JSONSchema
::register_format
(
1073 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1075 sub pmg_verify_transport_domain_or_nexthop
{
1076 my ($name, $noerr) = @_;
1078 if (pmg_verify_transport_domain
($name, 1)) {
1080 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1082 if ($nexthop =~ m/^\[(.*)\]$/) {
1085 return $name if pmg_verify_transport_address
($nexthop, 1);
1087 return undef if $noerr;
1088 die "value does not look like a valid domain or next-hop\n";
1092 sub read_tls_policy
{
1093 my ($filename, $fh) = @_;
1095 return {} if !defined($fh);
1097 my $tls_policy = {};
1099 while (defined(my $line = <$fh>)) {
1101 next if $line =~ m/^\s*$/;
1102 next if $line =~ m/^#(.*)\s*$/;
1104 my $parse_error = sub {
1106 die "parse error in '$filename': $line - $err";
1109 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1110 my ($destination, $policy) = ($1, $2);
1113 pmg_verify_transport_domain_or_nexthop
($destination);
1114 pmg_verify_tls_policy
($policy);
1117 $parse_error->($err);
1121 $tls_policy->{$destination} = {
1122 destination
=> $destination,
1126 $parse_error->('wrong format');
1133 sub write_tls_policy
{
1134 my ($filename, $fh, $tls_policy) = @_;
1136 return if !$tls_policy;
1138 foreach my $destination (sort keys %$tls_policy) {
1139 my $entry = $tls_policy->{$destination};
1140 PVE
::Tools
::safe_print
(
1141 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1145 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1146 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1149 undef, always_call_parser
=> 1);
1151 sub postmap_tls_policy
{
1152 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1155 my $transport_map_filename = "/etc/pmg/transport";
1157 sub postmap_pmg_transport
{
1158 PMG
::Utils
::run_postmap
($transport_map_filename);
1161 PVE
::JSONSchema
::register_format
(
1162 'transport-address', \
&pmg_verify_transport_address
);
1164 sub pmg_verify_transport_address
{
1165 my ($name, $noerr) = @_;
1167 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1169 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1172 return undef if $noerr;
1173 die "value does not look like a valid address\n";
1177 sub read_transport_map
{
1178 my ($filename, $fh) = @_;
1180 return [] if !defined($fh);
1186 while (defined(my $line = <$fh>)) {
1188 next if $line =~ m/^\s*$/;
1189 if ($line =~ m/^#(.*)\s*$/) {
1194 my $parse_error = sub {
1196 warn "parse error in '$filename': $line - $err";
1200 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1201 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1203 eval { pmg_verify_transport_domain_or_email
($domain); };
1205 $parse_error->($err);
1209 if ($host =~ m/^\[(.*)\]$/) {
1213 $use_mx = 0 if ($protocol eq "lmtp");
1215 eval { pmg_verify_transport_address
($host); };
1217 $parse_error->($err);
1223 protocol
=> $protocol,
1227 comment
=> $comment,
1229 $res->{$domain} = $data;
1232 $parse_error->('wrong format');
1239 sub write_transport_map
{
1240 my ($filename, $fh, $tmap) = @_;
1244 foreach my $domain (sort keys %$tmap) {
1245 my $data = $tmap->{$domain};
1247 my $comment = $data->{comment
};
1248 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1249 if defined($comment) && $comment !~ m/^\s*$/;
1251 my $bracket_host = !$data->{use_mx
};
1253 if ($data->{protocol
} eq 'lmtp') {
1255 $data->{protocol
} .= ":inet";
1257 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1258 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1260 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1264 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1265 \
&read_transport_map
,
1266 \
&write_transport_map
,
1267 undef, always_call_parser
=> 1);
1269 # config file generation using templates
1271 sub get_host_dns_info
{
1275 my $nodename = PVE
::INotify
::nodename
();
1277 $dnsinfo->{hostname
} = $nodename;
1278 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1280 my $domain = $resolv->{search
} // 'localdomain';
1281 # postfix will not parse a hostname with trailing '.'
1282 $domain =~ s/^(.*)\.$/$1/;
1283 $dnsinfo->{domain
} = $domain;
1285 $dnsinfo->{fqdn
} = "$nodename.$domain";
1290 sub get_template_vars
{
1293 my $vars = { pmg
=> $self->get_config() };
1295 my $dnsinfo = get_host_dns_info
();
1296 $vars->{dns
} = $dnsinfo;
1297 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1298 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1300 my $transportnets = {};
1306 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1307 foreach my $domain (keys %$tmap) {
1308 my $data = $tmap->{$domain};
1309 my $host = $data->{host
};
1310 if ($host =~ m/^$IPV4RE$/) {
1311 $transportnets->{"$host/32"} = 1;
1312 $mynetworks->{"$host/32"} = 1;
1313 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1314 $transportnets->{"[$1]/128"} = 1;
1315 $mynetworks->{"[$1]/128"} = 1;
1320 $vars->{postfix
}->{transportnets
} = join(' ', sort keys %$transportnets);
1322 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1323 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1324 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1325 $mynetworks->{"[$1]/$2"} = 1;
1327 $mynetworks->{$int_net_cidr} = 1;
1330 if ($int_ip =~ m/^$IPV6RE$/) {
1331 $mynetworks->{"[$int_ip]/128"} = 1;
1333 $mynetworks->{"$int_ip/32"} = 1;
1338 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1339 foreach my $cidr (keys %$netlist) {
1340 if ($cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1341 $mynetworks->{"[$1]/$2"} = 1;
1343 $mynetworks->{$cidr} = 1;
1347 # add default relay to mynetworks
1348 if (my $relay = $self->get('mail', 'relay')) {
1349 if ($relay =~ m/^$IPV4RE$/) {
1350 $mynetworks->{"$relay/32"} = 1;
1351 } elsif ($relay =~ m/^$IPV6RE$/) {
1352 $mynetworks->{"[$relay]/128"} = 1;
1354 # DNS name - do nothing ?
1358 $vars->{postfix
}->{mynetworks
} = join(' ', sort keys %$mynetworks);
1360 # normalize dnsbl_sites
1361 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1362 if (scalar(@dnsbl_sites)) {
1363 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1366 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1369 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1370 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1371 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1373 if (!defined($int_ip)) {
1374 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1375 $vars->{postfix
}->{int_ip
} = '127.0.0.1';
1376 } elsif ($int_ip =~ m/^$IPV6RE$/) {
1377 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1379 $vars->{postfix
}->{int_ip
} = $int_ip;
1382 my $wlbr = $dnsinfo->{fqdn
};
1383 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1386 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1388 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1390 my $uri = URI-
>new($proxy);
1391 my $host = $uri->host;
1392 my $port = $uri->port // 8080;
1394 my $data = { host
=> $host, port
=> $port };
1395 if (my $ui = $uri->userinfo) {
1396 my ($username, $pw) = split(/:/, $ui, 2);
1397 $data->{username
} = $username;
1398 $data->{password
} = $pw if defined($pw);
1400 $vars->{proxy
} = $data;
1403 warn "parse http_proxy failed - $@" if $@;
1405 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1410 # reads the $filename and checks if it's equal as the $cmp string passed
1411 my sub file_content_equals_str
{
1412 my ($filename, $cmp) = @_;
1414 return if !-f
$filename;
1415 my $current = PVE
::Tools
::file_get_contents
($filename, 128*1024);
1416 return defined($current) && $current eq $cmp; # no change
1419 # use one global TT cache
1420 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1422 my $template_toolkit;
1424 sub get_template_toolkit
{
1426 return $template_toolkit if $template_toolkit;
1428 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1430 return $template_toolkit;
1433 # rewrite file from template
1434 # return true if file has changed
1435 sub rewrite_config_file
{
1436 my ($self, $tmplname, $dstfn) = @_;
1438 my $demo = $self->get('admin', 'demo');
1441 my $demosrc = "$tmplname.demo";
1442 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1445 my ($perm, $uid, $gid);
1447 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1448 # needed if file contains a HTTPProxyPasswort
1450 $uid = getpwnam('clamav');
1451 $gid = getgrnam('adm');
1455 my $tt = get_template_toolkit
();
1457 my $vars = $self->get_template_vars();
1461 $tt->process($tmplname, $vars, \
$output) || die $tt->error() . "\n";
1463 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1465 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1467 if (defined($uid) && defined($gid)) {
1468 chown($uid, $gid, $dstfn);
1474 # rewrite spam configuration
1475 sub rewrite_config_spam
{
1478 my $use_awl = $self->get('spam', 'use_awl');
1479 my $use_bayes = $self->get('spam', 'use_bayes');
1480 my $use_razor = $self->get('spam', 'use_razor');
1484 # delete AW and bayes databases if those features are disabled
1486 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1490 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1491 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1492 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1495 # make sure we have the custom SA files (else cluster sync fails)
1496 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1497 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1499 $changes = 1 if $self->rewrite_config_file(
1500 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1502 $changes = 1 if $self->rewrite_config_file(
1503 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1505 $changes = 1 if $self->rewrite_config_file(
1506 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1508 $changes = 1 if $self->rewrite_config_file(
1509 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1512 mkdir "/root/.razor";
1514 $changes = 1 if $self->rewrite_config_file(
1515 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1517 if (! -e
'/root/.razor/identity') {
1520 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1521 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1524 syslog
('info', "registering razor failed: $err") if $err;
1531 # rewrite ClamAV configuration
1532 sub rewrite_config_clam
{
1535 return $self->rewrite_config_file(
1536 'clamd.conf.in', '/etc/clamav/clamd.conf');
1539 sub rewrite_config_freshclam
{
1542 return $self->rewrite_config_file(
1543 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1546 sub rewrite_config_postgres
{
1549 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1550 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1554 $changes = 1 if $self->rewrite_config_file(
1555 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1557 $changes = 1 if $self->rewrite_config_file(
1558 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1563 # rewrite /root/.forward
1564 sub rewrite_dot_forward
{
1567 my $dstfn = '/root/.forward';
1569 my $email = $self->get('admin', 'email');
1572 if ($email && $email =~ m/\s*(\S+)\s*/) {
1575 # empty .forward does not forward mails (see man local)
1577 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1579 PVE
::Tools
::file_set_contents
($dstfn, $output);
1584 my $write_smtp_whitelist = sub {
1585 my ($filename, $data, $action) = @_;
1587 $action = 'OK' if !$action;
1590 foreach my $k (sort keys %$data) {
1591 $new .= "$k $action\n";
1593 return 0 if file_content_equals_str
($filename, $new); # no change -> nothing to do
1595 PVE
::Tools
::file_set_contents
($filename, $new);
1597 PMG
::Utils
::run_postmap
($filename);
1602 sub rewrite_postfix_whitelist
{
1603 my ($rulecache) = @_;
1605 # see man page for regexp_table for postfix regex table format
1607 # we use a hash to avoid duplicate entries in regex tables
1610 my $clientlist = {};
1612 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1613 my $oclass = ref($obj);
1614 if ($oclass eq 'PMG::RuleDB::Receiver') {
1615 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1616 $tolist->{"/^$addr\$/"} = 1;
1617 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1618 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1619 $tolist->{"/^.+\@$addr\$/"} = 1;
1620 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1621 my $addr = $obj->{address
};
1623 $tolist->{"/^$addr\$/"} = 1;
1627 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1628 my $oclass = ref($obj);
1629 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1630 if ($oclass eq 'PMG::RuleDB::EMail') {
1631 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1632 $fromlist->{"/^$addr\$/"} = 1;
1633 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1634 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1635 $fromlist->{"/^.+\@$addr\$/"} = 1;
1636 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1637 my $addr = $obj->{address
};
1639 $fromlist->{"/^$addr\$/"} = 1;
1640 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1641 $clientlist->{$obj->{address
}} = 1;
1642 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1643 $clientlist->{$obj->{address
}} = 1;
1647 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1648 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1649 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1650 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1653 # rewrite /etc/postfix/*
1654 sub rewrite_config_postfix
{
1655 my ($self, $rulecache) = @_;
1657 # make sure we have required files (else postfix start fails)
1658 IO
::File-
>new($transport_map_filename, 'a', 0644);
1662 if ($self->get('mail', 'tls')) {
1664 PMG
::Utils
::gen_proxmox_tls_cert
();
1666 syslog
('info', "generating certificate failed: $@") if $@;
1669 $changes = 1 if $self->rewrite_config_file(
1670 'main.cf.in', '/etc/postfix/main.cf');
1672 $changes = 1 if $self->rewrite_config_file(
1673 'master.cf.in', '/etc/postfix/master.cf');
1675 # make sure we have required files (else postfix start fails)
1676 # Note: postmap need a valid /etc/postfix/main.cf configuration
1677 postmap_pmg_domains
();
1678 postmap_pmg_transport
();
1679 postmap_tls_policy
();
1681 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1683 # make sure aliases.db is up to date
1684 system('/usr/bin/newaliases');
1689 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1690 my $pmg_service_params = {
1698 dkim_sign_all_mail
=> 1,
1702 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1703 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1705 sub dump_smtp_filter_config
{
1710 foreach my $sec (sort keys %$pmg_service_params) {
1711 my $conf_sec = $self->{ids
}->{$sec} // {};
1712 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1713 $val = $conf_sec->{$key};
1714 $conf .= "$sec.$key:$val\n" if defined($val);
1721 sub compare_smtp_filter_config
{
1727 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1731 syslog
('warning', "reloading pmg-smtp-filter: $err");
1734 my $new = $self->dump_smtp_filter_config();
1735 $ret = 1 if $old ne $new;
1738 $self->write_smtp_filter_config() if $ret;
1743 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1745 sub write_smtp_filter_config
{
1748 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1749 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1750 $self->dump_smtp_filter_config());
1756 sub rewrite_config
{
1757 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1759 $force_restart = {} if ! $force_restart;
1761 my $log_restart = sub {
1762 syslog
('info', "configuration change detected for '$_[0]', restarting");
1765 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1766 $force_restart->{postfix
}) {
1767 $log_restart->('postfix');
1768 PMG
::Utils
::service_cmd
('postfix', 'reload');
1771 if ($self->rewrite_dot_forward() && $restart_services) {
1772 # no need to restart anything
1775 if ($self->rewrite_config_postgres() && $restart_services) {
1776 # do nothing (too many side effects)?
1777 # does not happen anyways, because config does not change.
1780 if (($self->rewrite_config_spam() && $restart_services) ||
1781 $force_restart->{spam
}) {
1782 $log_restart->('pmg-smtp-filter');
1783 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1786 if (($self->rewrite_config_clam() && $restart_services) ||
1787 $force_restart->{clam
}) {
1788 $log_restart->('clamav-daemon');
1789 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1792 if (($self->rewrite_config_freshclam() && $restart_services) ||
1793 $force_restart->{freshclam
}) {
1794 $log_restart->('clamav-freshclam');
1795 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1798 if (($self->compare_smtp_filter_config() && $restart_services) ||
1799 $force_restart->{spam
}) {
1800 syslog
('info', "scheduled reload for pmg-smtp-filter");
1801 PMG
::Utils
::reload_smtp_filter
();