1 package PMG
::Config
::Base
;
9 use PVE
::JSONSchema
qw(get_standard_option);
10 use PVE
::SectionConfig
;
13 use base
qw(PVE::SectionConfig);
17 type
=> { description
=> "Section type." },
19 description
=> "Section ID.",
20 type
=> 'string', format
=> 'pve-configid',
29 sub format_section_header
{
30 my ($class, $type, $sectionId) = @_;
32 die "internal error ($type ne $sectionId)" if $type ne $sectionId;
34 return "section: $type\n";
38 sub parse_section_header
{
39 my ($class, $line) = @_;
41 if ($line =~ m/^section:\s*(\S+)\s*$/) {
43 my $errmsg = undef; # set if you want to skip whole section
44 eval { PVE
::JSONSchema
::pve_verify_configid
($section); };
46 my $config = {}; # to return additional attributes
47 return ($section, $section, $errmsg, $config);
52 package PMG
::Config
::Admin
;
57 use base
qw(PMG::Config::Base);
66 description
=> "Use advanced filters for statistic.",
71 description
=> "Send daily reports.",
76 description
=> "User Statistics Lifetime (days)",
82 description
=> "Demo mode - do not start SMTP filter.",
87 description
=> "Administrator E-Mail address.",
88 type
=> 'string', format
=> 'email',
89 default => 'admin@domain.tld',
92 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
94 pattern
=> "http://.*",
97 description
=> "Use Avast Virus Scanner (/usr/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.",
102 description
=> "Use ClamAV Virus Scanner. This is the default virus scanner and is enabled by default.",
107 description
=> "Use Custom Check Script. The script has to take the defined arguments and can return Virus findings or a Spamscore.",
111 custom_check_path
=> {
112 description
=> "Absolute Path to the Custom Check Script",
113 type
=> 'string', pattern
=> '^/([^/\0]+\/)+[^/\0]+$',
114 default => '/usr/local/bin/pmg-custom-check',
117 description
=> "DKIM sign outbound mails with the configured Selector.",
121 dkim_sign_all_mail
=> {
122 description
=> "DKIM sign all outgoing mails irrespective of the Envelope From domain.",
127 description
=> "Default DKIM selector",
128 type
=> 'string', format
=> 'dns-name', #see RFC6376 3.1
135 advfilter
=> { optional
=> 1 },
136 avast
=> { optional
=> 1 },
137 clamav
=> { optional
=> 1 },
138 statlifetime
=> { optional
=> 1 },
139 dailyreport
=> { optional
=> 1 },
140 demo
=> { optional
=> 1 },
141 email
=> { optional
=> 1 },
142 http_proxy
=> { optional
=> 1 },
143 custom_check
=> { optional
=> 1 },
144 custom_check_path
=> { optional
=> 1 },
145 dkim_sign
=> { optional
=> 1 },
146 dkim_sign_all_mail
=> { optional
=> 1 },
147 dkim_selector
=> { optional
=> 1 },
151 package PMG
::Config
::Spam
;
156 use base
qw(PMG::Config::Base);
165 description
=> "This option is used to specify which languages are considered OK for incoming mail.",
167 pattern
=> '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
171 description
=> "Whether to use the naive-Bayesian-style classifier.",
176 description
=> "Use the Auto-Whitelist plugin.",
181 description
=> "Whether to use Razor2, if it is available.",
185 wl_bounce_relays
=> {
186 description
=> "Whitelist legitimate bounce relays.",
189 clamav_heuristic_score
=> {
190 description
=> "Score for ClamAV heuristics (Encrypted Archives/Documents, PhishingScanURLs, ...).",
197 description
=> "Additional score for bounce mails.",
204 description
=> "Enable real time blacklists (RBL) checks.",
209 description
=> "Maximum size of spam messages in bytes.",
215 description
=> "Extract text from attachments (doc, pdf, rtf, images) and scan for spam.",
224 use_awl
=> { optional
=> 1 },
225 use_razor
=> { optional
=> 1 },
226 wl_bounce_relays
=> { optional
=> 1 },
227 languages
=> { optional
=> 1 },
228 use_bayes
=> { optional
=> 1 },
229 clamav_heuristic_score
=> { optional
=> 1 },
230 bounce_score
=> { optional
=> 1 },
231 rbl_checks
=> { optional
=> 1 },
232 maxspamsize
=> { optional
=> 1 },
233 extract_text
=> { optional
=> 1 },
237 package PMG
::Config
::SpamQuarantine
;
242 use base
qw(PMG::Config::Base);
251 description
=> "Quarantine life time (days)",
257 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.",
259 enum
=> [qw(ticket ldap ldapticket)],
263 description
=> "Spam report style.",
265 enum
=> [qw(none short verbose custom)],
266 default => 'verbose',
269 description
=> "Allow to view images.",
274 description
=> "Allow to view hyperlinks.",
279 description
=> "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
280 type
=> 'string', format
=> 'address',
283 description
=> "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
290 description
=> "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
292 enum
=> [qw(http https)],
296 description
=> "Text for 'From' header in daily spam report mails.",
300 description
=> "Enables user self-service for Quarantine Links. Caution: this is accessible without authentication",
309 mailfrom
=> { optional
=> 1 },
310 hostname
=> { optional
=> 1 },
311 lifetime
=> { optional
=> 1 },
312 authmode
=> { optional
=> 1 },
313 reportstyle
=> { optional
=> 1 },
314 viewimages
=> { optional
=> 1 },
315 allowhrefs
=> { optional
=> 1 },
316 port
=> { optional
=> 1 },
317 protocol
=> { optional
=> 1 },
318 quarantinelink
=> { optional
=> 1 },
322 package PMG
::Config
::VirusQuarantine
;
327 use base
qw(PMG::Config::Base);
339 lifetime
=> { optional
=> 1 },
340 viewimages
=> { optional
=> 1 },
341 allowhrefs
=> { optional
=> 1 },
345 package PMG
::Config
::ClamAV
;
350 use base
qw(PMG::Config::Base);
359 description
=> "ClamAV database mirror server.",
361 default => 'database.clamav.net',
363 archiveblockencrypted
=> {
364 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'.",
369 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.",
375 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.",
381 description
=> "Files larger than this limit (in bytes) won't be scanned.",
387 description
=> "Sets the maximum amount of data (in bytes) to be scanned for each input file.",
390 default => 100000000,
393 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
398 # FIXME: remove for PMG 8.0 - https://blog.clamav.net/2021/04/are-you-still-attempting-to-download.html
400 description
=> "Enables support for Google Safe Browsing. (deprecated option, will be ignored)",
405 description
=> "Enables ScriptedUpdates (incremental download of signatures)",
414 archiveblockencrypted
=> { optional
=> 1 },
415 archivemaxrec
=> { optional
=> 1 },
416 archivemaxfiles
=> { optional
=> 1 },
417 archivemaxsize
=> { optional
=> 1 },
418 maxscansize
=> { optional
=> 1 },
419 dbmirror
=> { optional
=> 1 },
420 maxcccount
=> { optional
=> 1 },
421 safebrowsing
=> { optional
=> 1 }, # FIXME: remove for PMG 8.0
422 scriptedupdates
=> { optional
=> 1},
426 package PMG
::Config
::Mail
;
431 use PVE
::ProcFSTools
;
433 use base
qw(PMG::Config::Base);
440 sub physical_memory
{
442 return $physicalmem if $physicalmem;
444 my $info = PVE
::ProcFSTools
::read_meminfo
();
445 my $total = int($info->{memtotal
} / (1024*1024));
450 sub get_max_filters
{
451 # estimate optimal number of filter servers
455 my $memory = physical_memory
();
456 my $add_servers = int(($memory - 512)/$servermem);
457 $max_servers += $add_servers if $add_servers > 0;
458 $max_servers = 40 if $max_servers > 40;
460 return $max_servers - 2;
464 # estimate optimal number of smtpd daemons
466 my $max_servers = 25;
468 my $memory = physical_memory
();
469 my $add_servers = int(($memory - 512)/$servermem);
470 $max_servers += $add_servers if $add_servers > 0;
471 $max_servers = 100 if $max_servers > 100;
476 # estimate optimal number of proxpolicy servers
478 my $memory = physical_memory
();
479 $max_servers = 5 if $memory >= 500;
486 description
=> "SMTP port number for outgoing mail (trusted).",
493 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
500 description
=> "The default mail delivery transport (incoming mails).",
501 type
=> 'string', format
=> 'address',
504 description
=> "Transport protocol for relay host.",
506 enum
=> [qw(smtp lmtp)],
510 description
=> "SMTP/LMTP port number for relay host.",
517 description
=> "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
522 description
=> "When set, all outgoing mails are deliverd to the specified smarthost.",
523 type
=> 'string', format
=> 'address',
526 description
=> "SMTP port number for smarthost.",
533 description
=> "ESMTP banner.",
536 default => 'ESMTP Proxmox',
539 description
=> "Maximum number of pmg-smtp-filter processes.",
543 default => get_max_filters
(),
546 description
=> "Maximum number of pmgpolicy processes.",
550 default => get_max_policy
(),
553 description
=> "Maximum number of SMTP daemon processes (in).",
557 default => get_max_smtpd
(),
560 description
=> "Maximum number of SMTP daemon processes (out).",
564 default => get_max_smtpd
(),
566 conn_count_limit
=> {
567 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
573 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.",
578 message_rate_limit
=> {
579 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.",
585 description
=> "Hide received header in outgoing mails.",
590 description
=> "Maximum email size. Larger mails are rejected.",
593 default => 1024*1024*10,
596 description
=> "SMTP delay warning time (in hours).",
602 description
=> "Enable TLS.",
607 description
=> "Enable TLS Logging.",
612 description
=> "Add TLS received header.",
617 description
=> "Use Sender Policy Framework.",
622 description
=> "Use Greylisting for IPv4.",
627 description
=> "Netmask to apply for greylisting IPv4 hosts",
634 description
=> "Use Greylisting for IPv6.",
639 description
=> "Netmask to apply for greylisting IPv6 hosts",
646 description
=> "Use SMTP HELO tests.",
651 description
=> "Reject unknown clients.",
655 rejectunknownsender
=> {
656 description
=> "Reject unknown senders.",
661 description
=> "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
663 enum
=> ['450', '550'],
666 description
=> "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
667 type
=> 'string', format
=> 'dnsbl-entry-list',
670 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
675 before_queue_filtering
=> {
676 description
=> "Enable before queue filtering by pmg-smtp-filter",
681 description
=> "Send out NDR when mail gets blocked",
690 int_port
=> { optional
=> 1 },
691 ext_port
=> { optional
=> 1 },
692 smarthost
=> { optional
=> 1 },
693 smarthostport
=> { optional
=> 1 },
694 relay
=> { optional
=> 1 },
695 relayprotocol
=> { optional
=> 1 },
696 relayport
=> { optional
=> 1 },
697 relaynomx
=> { optional
=> 1 },
698 dwarning
=> { optional
=> 1 },
699 max_smtpd_in
=> { optional
=> 1 },
700 max_smtpd_out
=> { optional
=> 1 },
701 greylist
=> { optional
=> 1 },
702 greylistmask4
=> { optional
=> 1 },
703 greylist6
=> { optional
=> 1 },
704 greylistmask6
=> { optional
=> 1 },
705 helotests
=> { optional
=> 1 },
706 tls
=> { optional
=> 1 },
707 tlslog
=> { optional
=> 1 },
708 tlsheader
=> { optional
=> 1 },
709 spf
=> { optional
=> 1 },
710 maxsize
=> { optional
=> 1 },
711 banner
=> { optional
=> 1 },
712 max_filters
=> { optional
=> 1 },
713 max_policy
=> { optional
=> 1 },
714 hide_received
=> { optional
=> 1 },
715 rejectunknown
=> { optional
=> 1 },
716 rejectunknownsender
=> { optional
=> 1 },
717 conn_count_limit
=> { optional
=> 1 },
718 conn_rate_limit
=> { optional
=> 1 },
719 message_rate_limit
=> { optional
=> 1 },
720 verifyreceivers
=> { optional
=> 1 },
721 dnsbl_sites
=> { optional
=> 1 },
722 dnsbl_threshold
=> { optional
=> 1 },
723 before_queue_filtering
=> { optional
=> 1 },
724 ndr_on_block
=> { optional
=> 1 },
737 use PVE
::Tools
qw($IPV4RE $IPV6RE);
744 PMG
::Config
::Admin-
>register();
745 PMG
::Config
::Mail-
>register();
746 PMG
::Config
::SpamQuarantine-
>register();
747 PMG
::Config
::VirusQuarantine-
>register();
748 PMG
::Config
::Spam-
>register();
749 PMG
::Config
::ClamAV-
>register();
751 # initialize all plugins
752 PMG
::Config
::Base-
>init();
754 PVE
::JSONSchema
::register_format
(
755 'transport-domain', \
&pmg_verify_transport_domain
);
757 sub pmg_verify_transport_domain
{
758 my ($name, $noerr) = @_;
760 # like dns-name, but can contain leading dot
761 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
763 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
764 return undef if $noerr;
765 die "value does not look like a valid transport domain\n";
770 PVE
::JSONSchema
::register_format
(
771 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
773 sub pmg_verify_transport_domain_or_email
{
774 my ($name, $noerr) = @_;
776 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
779 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
783 # like dns-name, but can contain leading dot
784 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
785 return undef if $noerr;
786 die "value does not look like a valid transport domain or email address\n";
791 PVE
::JSONSchema
::register_format
(
792 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
794 sub pmg_verify_dnsbl_entry
{
795 my ($name, $noerr) = @_;
797 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
798 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
799 # we don't implement the ';' separated numbers in pattern, because this
800 # breaks at PVE::JSONSchema::split_list
801 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
803 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
804 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
805 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
806 return undef if $noerr;
807 die "value '$name' does not look like a valid dnsbl entry\n";
815 my $class = ref($type) || $type;
817 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
819 return bless $cfg, $class;
825 PVE
::INotify
::write_file
("pmg.conf", $self);
828 my $lockfile = "/var/lock/pmgconfig.lck";
831 my ($code, $errmsg) = @_;
833 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
835 $errmsg ?
die "$errmsg: $err" : die $err;
841 my ($self, $section, $key, $value) = @_;
843 my $pdata = PMG
::Config
::Base-
>private();
845 my $plugin = $pdata->{plugins
}->{$section};
846 die "no such section '$section'" if !$plugin;
848 if (defined($value)) {
849 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
850 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
851 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
853 if (defined($self->{ids
}->{$section})) {
854 delete $self->{ids
}->{$section}->{$key};
861 # get section value or default
863 my ($self, $section, $key, $nodefault) = @_;
865 my $pdata = PMG
::Config
::Base-
>private();
866 my $pdesc = $pdata->{propertyList
}->{$key};
867 die "no such property '$section/$key'\n"
868 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
869 defined($pdata->{options
}->{$section}->{$key}));
871 if (defined($self->{ids
}->{$section}) &&
872 defined(my $value = $self->{ids
}->{$section}->{$key})) {
876 return undef if $nodefault;
878 return $pdesc->{default};
881 # get a whole section with default value
883 my ($self, $section) = @_;
885 my $pdata = PMG
::Config
::Base-
>private();
886 return undef if !defined($pdata->{options
}->{$section});
890 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
892 my $pdesc = $pdata->{propertyList
}->{$key};
894 if (defined($self->{ids
}->{$section}) &&
895 defined(my $value = $self->{ids
}->{$section}->{$key})) {
896 $res->{$key} = $value;
899 $res->{$key} = $pdesc->{default};
905 # get a whole config with default values
909 my $pdata = PMG
::Config
::Base-
>private();
913 foreach my $type (keys %{$pdata->{plugins
}}) {
914 my $plugin = $pdata->{plugins
}->{$type};
915 $res->{$type} = $self->get_section($type);
922 my ($filename, $fh) = @_;
924 local $/ = undef; # slurp mode
927 $raw = <$fh> if defined($fh);
929 return PMG
::Config
::Base-
>parse_config($filename, $raw);
933 my ($filename, $fh, $cfg) = @_;
935 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
937 PVE
::Tools
::safe_print
($filename, $fh, $raw);
940 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
943 undef, always_call_parser
=> 1);
945 # parsers/writers for other files
947 my $domainsfilename = "/etc/pmg/domains";
949 sub postmap_pmg_domains
{
950 PMG
::Utils
::run_postmap
($domainsfilename);
953 sub read_pmg_domains
{
954 my ($filename, $fh) = @_;
960 while (defined(my $line = <$fh>)) {
962 next if $line =~ m/^\s*$/;
963 if ($line =~ m/^#(.*)\s*$/) {
967 if ($line =~ m/^(\S+)\s.*$/) {
969 $domains->{$domain} = {
970 domain
=> $domain, comment
=> $comment };
973 warn "parse error in '$filename': $line\n";
982 sub write_pmg_domains
{
983 my ($filename, $fh, $domains) = @_;
985 foreach my $domain (sort keys %$domains) {
986 my $comment = $domains->{$domain}->{comment
};
987 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
988 if defined($comment) && $comment !~ m/^\s*$/;
990 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
994 PVE
::INotify
::register_file
('domains', $domainsfilename,
997 undef, always_call_parser
=> 1);
999 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
1001 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
1003 \
&write_pmg_domains
,
1004 undef, always_call_parser
=> 1);
1006 my $mynetworks_filename = "/etc/pmg/mynetworks";
1008 sub read_pmg_mynetworks
{
1009 my ($filename, $fh) = @_;
1011 my $mynetworks = {};
1015 while (defined(my $line = <$fh>)) {
1017 next if $line =~ m/^\s*$/;
1018 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1019 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1020 my $cidr = "$network/${prefix_size}";
1021 # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
1022 $mynetworks->{$cidr} = {
1024 network_address
=> $network,
1025 prefix_size
=> $prefix_size,
1026 comment
=> $comment // '',
1029 warn "parse error in '$filename': $line\n";
1037 sub write_pmg_mynetworks
{
1038 my ($filename, $fh, $mynetworks) = @_;
1040 foreach my $cidr (sort keys %$mynetworks) {
1041 my $data = $mynetworks->{$cidr};
1042 my $comment = $data->{comment
} // '*';
1043 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1047 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1048 \
&read_pmg_mynetworks
,
1049 \
&write_pmg_mynetworks
,
1050 undef, always_call_parser
=> 1);
1052 PVE
::JSONSchema
::register_format
(
1053 'tls-policy', \
&pmg_verify_tls_policy
);
1055 # TODO: extend to parse attributes of the policy
1056 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1057 sub pmg_verify_tls_policy
{
1058 my ($policy, $noerr) = @_;
1060 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1061 return undef if $noerr;
1062 die "value '$policy' does not look like a valid tls policy\n";
1067 PVE
::JSONSchema
::register_format
(
1068 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1070 sub pmg_verify_tls_policy_strict
{
1071 my ($policy, $noerr) = @_;
1073 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1074 return undef if $noerr;
1075 die "value '$policy' does not look like a valid tls policy\n";
1080 PVE
::JSONSchema
::register_format
(
1081 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1083 sub pmg_verify_transport_domain_or_nexthop
{
1084 my ($name, $noerr) = @_;
1086 if (pmg_verify_transport_domain
($name, 1)) {
1088 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1090 if ($nexthop =~ m/^\[(.*)\]$/) {
1093 return $name if pmg_verify_transport_address
($nexthop, 1);
1095 return undef if $noerr;
1096 die "value does not look like a valid domain or next-hop\n";
1100 sub read_tls_policy
{
1101 my ($filename, $fh) = @_;
1103 return {} if !defined($fh);
1105 my $tls_policy = {};
1107 while (defined(my $line = <$fh>)) {
1109 next if $line =~ m/^\s*$/;
1110 next if $line =~ m/^#(.*)\s*$/;
1112 my $parse_error = sub {
1114 warn "parse error in '$filename': $line - $err\n";
1117 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1118 my ($destination, $policy) = ($1, $2);
1121 pmg_verify_transport_domain_or_nexthop
($destination);
1122 pmg_verify_tls_policy
($policy);
1125 $parse_error->($err);
1129 $tls_policy->{$destination} = {
1130 destination
=> $destination,
1134 $parse_error->('wrong format');
1141 sub write_tls_policy
{
1142 my ($filename, $fh, $tls_policy) = @_;
1144 return if !$tls_policy;
1146 foreach my $destination (sort keys %$tls_policy) {
1147 my $entry = $tls_policy->{$destination};
1148 PVE
::Tools
::safe_print
(
1149 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1153 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1154 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1157 undef, always_call_parser
=> 1);
1159 sub postmap_tls_policy
{
1160 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1163 sub read_tls_inbound_domains
{
1164 my ($filename, $fh) = @_;
1166 return {} if !defined($fh);
1170 while (defined(my $line = <$fh>)) {
1172 next if $line =~ m/^\s*$/;
1173 next if $line =~ m/^#(.*)\s*$/;
1175 my $parse_error = sub {
1177 warn "parse error in '$filename': $line - $err\n";
1180 if ($line =~ m/^(\S+) reject_plaintext_session$/) {
1183 eval { pmg_verify_transport_domain
($domain) };
1185 $parse_error->($err);
1189 $domains->{$domain} = 1;
1191 $parse_error->('wrong format');
1198 sub write_tls_inbound_domains
{
1199 my ($filename, $fh, $domains) = @_;
1201 return if !$domains;
1203 foreach my $domain (sort keys %$domains) {
1204 PVE
::Tools
::safe_print
($filename, $fh, "$domain reject_plaintext_session\n");
1208 my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
1209 PVE
::INotify
::register_file
('tls_inbound_domains', $tls_inbound_domains_map_filename,
1210 \
&read_tls_inbound_domains
,
1211 \
&write_tls_inbound_domains
,
1212 undef, always_call_parser
=> 1);
1214 sub postmap_tls_inbound_domains
{
1215 PMG
::Utils
::run_postmap
($tls_inbound_domains_map_filename);
1218 my $transport_map_filename = "/etc/pmg/transport";
1220 sub postmap_pmg_transport
{
1221 PMG
::Utils
::run_postmap
($transport_map_filename);
1224 PVE
::JSONSchema
::register_format
(
1225 'transport-address', \
&pmg_verify_transport_address
);
1227 sub pmg_verify_transport_address
{
1228 my ($name, $noerr) = @_;
1230 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1232 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1235 return undef if $noerr;
1236 die "value does not look like a valid address\n";
1240 sub read_transport_map
{
1241 my ($filename, $fh) = @_;
1243 return [] if !defined($fh);
1249 while (defined(my $line = <$fh>)) {
1251 next if $line =~ m/^\s*$/;
1252 if ($line =~ m/^#(.*)\s*$/) {
1257 my $parse_error = sub {
1259 warn "parse error in '$filename': $line - $err";
1263 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1264 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1266 eval { pmg_verify_transport_domain_or_email
($domain); };
1268 $parse_error->($err);
1272 if ($host =~ m/^\[(.*)\]$/) {
1276 $use_mx = 0 if ($protocol eq "lmtp");
1278 eval { pmg_verify_transport_address
($host); };
1280 $parse_error->($err);
1286 protocol
=> $protocol,
1290 comment
=> $comment,
1292 $res->{$domain} = $data;
1295 $parse_error->('wrong format');
1302 sub write_transport_map
{
1303 my ($filename, $fh, $tmap) = @_;
1307 foreach my $domain (sort keys %$tmap) {
1308 my $data = $tmap->{$domain};
1310 my $comment = $data->{comment
};
1311 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1312 if defined($comment) && $comment !~ m/^\s*$/;
1314 my $bracket_host = !$data->{use_mx
};
1316 if ($data->{protocol
} eq 'lmtp') {
1318 $data->{protocol
} .= ":inet";
1320 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1321 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1323 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1327 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1328 \
&read_transport_map
,
1329 \
&write_transport_map
,
1330 undef, always_call_parser
=> 1);
1332 # config file generation using templates
1334 sub get_host_dns_info
{
1338 my $nodename = PVE
::INotify
::nodename
();
1340 $dnsinfo->{hostname
} = $nodename;
1341 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1343 my $domain = $resolv->{search
} // 'localdomain';
1344 # postfix will not parse a hostname with trailing '.'
1345 $domain =~ s/^(.*)\.$/$1/;
1346 $dnsinfo->{domain
} = $domain;
1348 $dnsinfo->{fqdn
} = "$nodename.$domain";
1353 sub get_template_vars
{
1356 my $vars = { pmg
=> $self->get_config() };
1358 my $dnsinfo = get_host_dns_info
();
1359 $vars->{dns
} = $dnsinfo;
1360 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1361 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1363 my $transportnets = {};
1369 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1370 foreach my $domain (keys %$tmap) {
1371 my $data = $tmap->{$domain};
1372 my $host = $data->{host
};
1373 if ($host =~ m/^$IPV4RE$/) {
1374 $transportnets->{"$host/32"} = 1;
1375 $mynetworks->{"$host/32"} = 1;
1376 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1377 $transportnets->{"[$1]/128"} = 1;
1378 $mynetworks->{"[$1]/128"} = 1;
1383 $vars->{postfix
}->{transportnets
} = join(' ', sort keys %$transportnets);
1385 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1386 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1387 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1388 $mynetworks->{"[$1]/$2"} = 1;
1390 $mynetworks->{$int_net_cidr} = 1;
1393 if ($int_ip =~ m/^$IPV6RE$/) {
1394 $mynetworks->{"[$int_ip]/128"} = 1;
1396 $mynetworks->{"$int_ip/32"} = 1;
1401 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1402 foreach my $cidr (keys %$netlist) {
1403 my $ip = PVE
::Network
::IP_from_cidr
($cidr);
1406 warn "failed to parse mynetworks entry '$cidr', ignoring\n";
1407 } elsif ($ip->version() == 4) {
1408 $mynetworks->{$ip->prefix()} = 1;
1410 my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
1411 $mynetworks->{$address} = 1;
1415 # add default relay to mynetworks
1416 if (my $relay = $self->get('mail', 'relay')) {
1417 if ($relay =~ m/^$IPV4RE$/) {
1418 $mynetworks->{"$relay/32"} = 1;
1419 } elsif ($relay =~ m/^$IPV6RE$/) {
1420 $mynetworks->{"[$relay]/128"} = 1;
1422 # DNS name - do nothing ?
1426 $vars->{postfix
}->{mynetworks
} = join(' ', sort keys %$mynetworks);
1428 # normalize dnsbl_sites
1429 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1430 if (scalar(@dnsbl_sites)) {
1431 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1434 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1437 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1438 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1439 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1441 if (!defined($int_ip)) {
1442 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1443 $vars->{postfix
}->{int_ip
} = '127.0.0.1';
1444 } elsif ($int_ip =~ m/^$IPV6RE$/) {
1445 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1447 $vars->{postfix
}->{int_ip
} = $int_ip;
1450 my $wlbr = $dnsinfo->{fqdn
};
1451 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1454 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1456 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1458 my $uri = URI-
>new($proxy);
1459 my $host = $uri->host;
1460 my $port = $uri->port // 8080;
1462 my $data = { host
=> $host, port
=> $port };
1463 if (my $ui = $uri->userinfo) {
1464 my ($username, $pw) = split(/:/, $ui, 2);
1465 $data->{username
} = $username;
1466 $data->{password
} = $pw if defined($pw);
1468 $vars->{proxy
} = $data;
1471 warn "parse http_proxy failed - $@" if $@;
1473 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1478 # reads the $filename and checks if it's equal as the $cmp string passed
1479 my sub file_content_equals_str
{
1480 my ($filename, $cmp) = @_;
1482 return if !-f
$filename;
1483 my $current = PVE
::Tools
::file_get_contents
($filename, 128*1024);
1484 return defined($current) && $current eq $cmp; # no change
1487 # use one global TT cache
1488 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1490 my $template_toolkit;
1492 sub get_template_toolkit
{
1494 return $template_toolkit if $template_toolkit;
1496 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1498 return $template_toolkit;
1501 # rewrite file from template
1502 # return true if file has changed
1503 sub rewrite_config_file
{
1504 my ($self, $tmplname, $dstfn) = @_;
1506 my $demo = $self->get('admin', 'demo');
1509 my $demosrc = "$tmplname.demo";
1510 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1513 my ($perm, $uid, $gid);
1515 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1516 # needed if file contains a HTTPProxyPasswort
1518 $uid = getpwnam('clamav');
1519 $gid = getgrnam('adm');
1523 my $tt = get_template_toolkit
();
1525 my $vars = $self->get_template_vars();
1529 $tt->process($tmplname, $vars, \
$output) || die $tt->error() . "\n";
1531 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1533 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1535 if (defined($uid) && defined($gid)) {
1536 chown($uid, $gid, $dstfn);
1542 # rewrite spam configuration
1543 sub rewrite_config_spam
{
1546 my $use_awl = $self->get('spam', 'use_awl');
1547 my $use_bayes = $self->get('spam', 'use_bayes');
1548 my $use_razor = $self->get('spam', 'use_razor');
1552 # delete AW and bayes databases if those features are disabled
1554 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1558 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1559 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1560 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1563 # make sure we have the custom SA files (else cluster sync fails)
1564 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1565 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1567 $changes = 1 if $self->rewrite_config_file(
1568 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1570 $changes = 1 if $self->rewrite_config_file(
1571 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1573 $changes = 1 if $self->rewrite_config_file(
1574 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1576 $changes = 1 if $self->rewrite_config_file(
1577 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1579 $changes = 1 if $self->rewrite_config_file(
1580 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
1582 $changes = 1 if $self->rewrite_config_file(
1583 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
1586 mkdir "/root/.razor";
1588 $changes = 1 if $self->rewrite_config_file(
1589 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1591 if (! -e
'/root/.razor/identity') {
1594 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1595 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1598 syslog
('info', "registering razor failed: $err") if $err;
1605 # rewrite ClamAV configuration
1606 sub rewrite_config_clam
{
1609 return $self->rewrite_config_file(
1610 'clamd.conf.in', '/etc/clamav/clamd.conf');
1613 sub rewrite_config_freshclam
{
1616 return $self->rewrite_config_file(
1617 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1620 sub rewrite_config_postgres
{
1623 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1624 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1628 $changes = 1 if $self->rewrite_config_file(
1629 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1631 $changes = 1 if $self->rewrite_config_file(
1632 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1637 # rewrite /root/.forward
1638 sub rewrite_dot_forward
{
1641 my $dstfn = '/root/.forward';
1643 my $email = $self->get('admin', 'email');
1646 if ($email && $email =~ m/\s*(\S+)\s*/) {
1649 # empty .forward does not forward mails (see man local)
1651 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1653 PVE
::Tools
::file_set_contents
($dstfn, $output);
1658 my $write_smtp_whitelist = sub {
1659 my ($filename, $data, $action) = @_;
1661 $action = 'OK' if !$action;
1664 foreach my $k (sort keys %$data) {
1665 $new .= "$k $action\n";
1667 return 0 if file_content_equals_str
($filename, $new); # no change -> nothing to do
1669 PVE
::Tools
::file_set_contents
($filename, $new);
1671 PMG
::Utils
::run_postmap
($filename);
1676 sub rewrite_postfix_whitelist
{
1677 my ($rulecache) = @_;
1679 # see man page for regexp_table for postfix regex table format
1681 # we use a hash to avoid duplicate entries in regex tables
1684 my $clientlist = {};
1686 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1687 my $oclass = ref($obj);
1688 if ($oclass eq 'PMG::RuleDB::Receiver') {
1689 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1690 $tolist->{"/^$addr\$/"} = 1;
1691 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1692 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1693 $tolist->{"/^.+\@$addr\$/"} = 1;
1694 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1695 my $addr = $obj->{address
};
1697 $tolist->{"/^$addr\$/"} = 1;
1701 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1702 my $oclass = ref($obj);
1703 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1704 if ($oclass eq 'PMG::RuleDB::EMail') {
1705 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1706 $fromlist->{"/^$addr\$/"} = 1;
1707 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1708 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1709 $fromlist->{"/^.+\@$addr\$/"} = 1;
1710 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1711 my $addr = $obj->{address
};
1713 $fromlist->{"/^$addr\$/"} = 1;
1714 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1715 $clientlist->{$obj->{address
}} = 1;
1716 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1717 $clientlist->{$obj->{address
}} = 1;
1721 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1722 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1723 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1724 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1727 # rewrite /etc/postfix/*
1728 sub rewrite_config_postfix
{
1729 my ($self, $rulecache) = @_;
1731 # make sure we have required files (else postfix start fails)
1732 IO
::File-
>new($transport_map_filename, 'a', 0644);
1736 if ($self->get('mail', 'tls')) {
1738 PMG
::Utils
::gen_proxmox_tls_cert
();
1740 syslog
('info', "generating certificate failed: $@") if $@;
1743 $changes = 1 if $self->rewrite_config_file(
1744 'main.cf.in', '/etc/postfix/main.cf');
1746 $changes = 1 if $self->rewrite_config_file(
1747 'master.cf.in', '/etc/postfix/master.cf');
1749 # make sure we have required files (else postfix start fails)
1750 # Note: postmap need a valid /etc/postfix/main.cf configuration
1751 postmap_pmg_domains
();
1752 postmap_pmg_transport
();
1753 postmap_tls_policy
();
1754 postmap_tls_inbound_domains
();
1756 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1758 # make sure aliases.db is up to date
1759 system('/usr/bin/newaliases');
1764 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1765 my $pmg_service_params = {
1773 dkim_sign_all_mail
=> 1,
1777 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1778 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1780 sub dump_smtp_filter_config
{
1785 foreach my $sec (sort keys %$pmg_service_params) {
1786 my $conf_sec = $self->{ids
}->{$sec} // {};
1787 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1788 $val = $conf_sec->{$key};
1789 $conf .= "$sec.$key:$val\n" if defined($val);
1796 sub compare_smtp_filter_config
{
1802 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1806 syslog
('warning', "reloading pmg-smtp-filter: $err");
1809 my $new = $self->dump_smtp_filter_config();
1810 $ret = 1 if $old ne $new;
1813 $self->write_smtp_filter_config() if $ret;
1818 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1820 sub write_smtp_filter_config
{
1823 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1824 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1825 $self->dump_smtp_filter_config());
1831 sub rewrite_config
{
1832 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1834 $force_restart = {} if ! $force_restart;
1836 my $log_restart = sub {
1837 syslog
('info', "configuration change detected for '$_[0]', restarting");
1840 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1841 $force_restart->{postfix
}) {
1842 $log_restart->('postfix');
1843 PMG
::Utils
::service_cmd
('postfix', 'reload');
1846 if ($self->rewrite_dot_forward() && $restart_services) {
1847 # no need to restart anything
1850 if ($self->rewrite_config_postgres() && $restart_services) {
1851 # do nothing (too many side effects)?
1852 # does not happen anyways, because config does not change.
1855 if (($self->rewrite_config_spam() && $restart_services) ||
1856 $force_restart->{spam
}) {
1857 $log_restart->('pmg-smtp-filter');
1858 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1861 if (($self->rewrite_config_clam() && $restart_services) ||
1862 $force_restart->{clam
}) {
1863 $log_restart->('clamav-daemon');
1864 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1867 if (($self->rewrite_config_freshclam() && $restart_services) ||
1868 $force_restart->{freshclam
}) {
1869 $log_restart->('clamav-freshclam');
1870 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1873 if (($self->compare_smtp_filter_config() && $restart_services) ||
1874 $force_restart->{spam
}) {
1875 syslog
('info', "scheduled reload for pmg-smtp-filter");
1876 PMG
::Utils
::reload_smtp_filter
();