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
=> "Enable advanced filters for statistic.",
67 verbose_description
=> <<EODESC,
68 Enable advanced filters for statistic.
70 If this is enabled, the receiver statistic are limited to active ones
71 (receivers which also sent out mail in the 90 days before), and the contact
72 statistic will not contain these active receivers.
78 description
=> "Send daily reports.",
83 description
=> "User Statistics Lifetime (days)",
89 description
=> "Demo mode - do not start SMTP filter.",
94 description
=> "Administrator E-Mail address.",
95 type
=> 'string', format
=> 'email',
96 default => 'admin@domain.tld',
99 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
101 pattern
=> "http://.*",
104 description
=> "Use Avast Virus Scanner (/usr/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.",
109 description
=> "Use ClamAV Virus Scanner. This is the default virus scanner and is enabled by default.",
114 description
=> "Use Custom Check Script. The script has to take the defined arguments and can return Virus findings or a Spamscore.",
118 custom_check_path
=> {
119 description
=> "Absolute Path to the Custom Check Script",
120 type
=> 'string', pattern
=> '^/([^/\0]+\/)+[^/\0]+$',
121 default => '/usr/local/bin/pmg-custom-check',
124 description
=> "DKIM sign outbound mails with the configured Selector.",
128 dkim_sign_all_mail
=> {
129 description
=> "DKIM sign all outgoing mails irrespective of the Envelope From domain.",
134 description
=> "Default DKIM selector",
135 type
=> 'string', format
=> 'dns-name', #see RFC6376 3.1
142 advfilter
=> { optional
=> 1 },
143 avast
=> { optional
=> 1 },
144 clamav
=> { optional
=> 1 },
145 statlifetime
=> { optional
=> 1 },
146 dailyreport
=> { optional
=> 1 },
147 demo
=> { optional
=> 1 },
148 email
=> { optional
=> 1 },
149 http_proxy
=> { optional
=> 1 },
150 custom_check
=> { optional
=> 1 },
151 custom_check_path
=> { optional
=> 1 },
152 dkim_sign
=> { optional
=> 1 },
153 dkim_sign_all_mail
=> { optional
=> 1 },
154 dkim_selector
=> { optional
=> 1 },
158 package PMG
::Config
::Spam
;
163 use base
qw(PMG::Config::Base);
172 description
=> "This option is used to specify which languages are considered OK for incoming mail.",
174 pattern
=> '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
178 description
=> "Whether to use the naive-Bayesian-style classifier.",
183 description
=> "Use the Auto-Whitelist plugin.",
188 description
=> "Whether to use Razor2, if it is available.",
192 wl_bounce_relays
=> {
193 description
=> "Whitelist legitimate bounce relays.",
196 clamav_heuristic_score
=> {
197 description
=> "Score for ClamAV heuristics (Encrypted Archives/Documents, PhishingScanURLs, ...).",
204 description
=> "Additional score for bounce mails.",
211 description
=> "Enable real time blacklists (RBL) checks.",
216 description
=> "Maximum size of spam messages in bytes.",
222 description
=> "Extract text from attachments (doc, pdf, rtf, images) and scan for spam.",
231 use_awl
=> { optional
=> 1 },
232 use_razor
=> { optional
=> 1 },
233 wl_bounce_relays
=> { optional
=> 1 },
234 languages
=> { optional
=> 1 },
235 use_bayes
=> { optional
=> 1 },
236 clamav_heuristic_score
=> { optional
=> 1 },
237 bounce_score
=> { optional
=> 1 },
238 rbl_checks
=> { optional
=> 1 },
239 maxspamsize
=> { optional
=> 1 },
240 extract_text
=> { optional
=> 1 },
244 package PMG
::Config
::SpamQuarantine
;
249 use base
qw(PMG::Config::Base);
258 description
=> "Quarantine life time (days)",
264 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.",
266 enum
=> [qw(ticket ldap ldapticket)],
270 description
=> "Spam report style.",
272 enum
=> [qw(none short verbose custom)],
273 default => 'verbose',
276 description
=> "Allow to view images.",
281 description
=> "Allow to view hyperlinks.",
286 description
=> "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
287 type
=> 'string', format
=> 'address',
290 description
=> "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
297 description
=> "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
299 enum
=> [qw(http https)],
303 description
=> "Text for 'From' header in daily spam report mails.",
307 description
=> "Enables user self-service for Quarantine Links. Caution: this is accessible without authentication",
316 mailfrom
=> { optional
=> 1 },
317 hostname
=> { optional
=> 1 },
318 lifetime
=> { optional
=> 1 },
319 authmode
=> { optional
=> 1 },
320 reportstyle
=> { optional
=> 1 },
321 viewimages
=> { optional
=> 1 },
322 allowhrefs
=> { optional
=> 1 },
323 port
=> { optional
=> 1 },
324 protocol
=> { optional
=> 1 },
325 quarantinelink
=> { optional
=> 1 },
329 package PMG
::Config
::VirusQuarantine
;
334 use base
qw(PMG::Config::Base);
346 lifetime
=> { optional
=> 1 },
347 viewimages
=> { optional
=> 1 },
348 allowhrefs
=> { optional
=> 1 },
352 package PMG
::Config
::ClamAV
;
357 use base
qw(PMG::Config::Base);
366 description
=> "ClamAV database mirror server.",
368 default => 'database.clamav.net',
370 archiveblockencrypted
=> {
371 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'.",
376 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.",
382 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.",
388 description
=> "Files larger than this limit (in bytes) won't be scanned.",
394 description
=> "Sets the maximum amount of data (in bytes) to be scanned for each input file.",
397 default => 100000000,
400 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
405 # FIXME: remove for PMG 8.0 - https://blog.clamav.net/2021/04/are-you-still-attempting-to-download.html
407 description
=> "Enables support for Google Safe Browsing. (deprecated option, will be ignored)",
412 description
=> "Enables ScriptedUpdates (incremental download of signatures)",
421 archiveblockencrypted
=> { optional
=> 1 },
422 archivemaxrec
=> { optional
=> 1 },
423 archivemaxfiles
=> { optional
=> 1 },
424 archivemaxsize
=> { optional
=> 1 },
425 maxscansize
=> { optional
=> 1 },
426 dbmirror
=> { optional
=> 1 },
427 maxcccount
=> { optional
=> 1 },
428 safebrowsing
=> { optional
=> 1 }, # FIXME: remove for PMG 8.0
429 scriptedupdates
=> { optional
=> 1},
433 package PMG
::Config
::Mail
;
438 use PVE
::ProcFSTools
;
440 use base
qw(PMG::Config::Base);
447 sub physical_memory
{
449 return $physicalmem if $physicalmem;
451 my $info = PVE
::ProcFSTools
::read_meminfo
();
452 my $total = int($info->{memtotal
} / (1024*1024));
457 sub get_max_filters
{
458 # estimate optimal number of filter servers
462 my $memory = physical_memory
();
463 my $add_servers = int(($memory - 512)/$servermem);
464 $max_servers += $add_servers if $add_servers > 0;
465 $max_servers = 40 if $max_servers > 40;
467 return $max_servers - 2;
471 # estimate optimal number of smtpd daemons
473 my $max_servers = 25;
475 my $memory = physical_memory
();
476 my $add_servers = int(($memory - 512)/$servermem);
477 $max_servers += $add_servers if $add_servers > 0;
478 $max_servers = 100 if $max_servers > 100;
483 # estimate optimal number of proxpolicy servers
485 my $memory = physical_memory
();
486 $max_servers = 5 if $memory >= 500;
493 description
=> "SMTP port number for outgoing mail (trusted).",
500 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
507 description
=> "The default mail delivery transport (incoming mails).",
508 type
=> 'string', format
=> 'address',
511 description
=> "Transport protocol for relay host.",
513 enum
=> [qw(smtp lmtp)],
517 description
=> "SMTP/LMTP port number for relay host.",
524 description
=> "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
529 description
=> "When set, all outgoing mails are deliverd to the specified smarthost."
530 ." (postfix option `default_transport`)",
531 type
=> 'string', format
=> 'address',
534 description
=> "SMTP port number for smarthost. (postfix option `default_transport`)",
541 description
=> "ESMTP banner.",
544 default => 'ESMTP Proxmox',
547 description
=> "Maximum number of pmg-smtp-filter processes.",
551 default => get_max_filters
(),
554 description
=> "Maximum number of pmgpolicy processes.",
558 default => get_max_policy
(),
561 description
=> "Maximum number of SMTP daemon processes (in).",
565 default => get_max_smtpd
(),
568 description
=> "Maximum number of SMTP daemon processes (out).",
572 default => get_max_smtpd
(),
574 conn_count_limit
=> {
575 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
581 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.",
586 message_rate_limit
=> {
587 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.",
593 description
=> "Hide received header in outgoing mails.",
598 description
=> "Maximum email size. Larger mails are rejected. (postfix option `message_size_limit`)",
601 default => 1024*1024*10,
604 description
=> "SMTP delay warning time (in hours). (postfix option `delay_warning_time`)",
610 description
=> "Enable TLS.",
615 description
=> "Enable TLS Logging.",
620 description
=> "Add TLS received header.",
625 description
=> "Use Sender Policy Framework.",
630 description
=> "Use Greylisting for IPv4.",
635 description
=> "Netmask to apply for greylisting IPv4 hosts",
642 description
=> "Use Greylisting for IPv6.",
647 description
=> "Netmask to apply for greylisting IPv6 hosts",
654 description
=> "Use SMTP HELO tests. (postfix option `smtpd_helo_restrictions`)",
659 description
=> "Reject unknown clients. (postfix option `reject_unknown_client_hostname`)",
663 rejectunknownsender
=> {
664 description
=> "Reject unknown senders. (postfix option `reject_unknown_sender_domain`)",
669 description
=> "Enable receiver verification. The value spefifies the numerical reply"
670 ." code when the Postfix SMTP server rejects a recipient address."
671 ." (postfix options `reject_unknown_recipient_domain`, `reject_unverified_recipient`,"
672 ." and `unverified_recipient_reject_code`)",
674 enum
=> ['450', '550'],
677 description
=> "Optional list of DNS white/blacklist domains (postfix option `postscreen_dnsbl_sites`).",
678 type
=> 'string', format
=> 'dnsbl-entry-list',
681 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on"
682 ." its combined DNSBL score (postfix option `postscreen_dnsbl_threshold`).",
687 before_queue_filtering
=> {
688 description
=> "Enable before queue filtering by pmg-smtp-filter",
693 description
=> "Send out NDR when mail gets blocked",
698 description
=> "Enable SMTPUTF8 support in Postfix and detection for locally generated mail (postfix option `smtputf8_enable`)",
707 int_port
=> { optional
=> 1 },
708 ext_port
=> { optional
=> 1 },
709 smarthost
=> { optional
=> 1 },
710 smarthostport
=> { optional
=> 1 },
711 relay
=> { optional
=> 1 },
712 relayprotocol
=> { optional
=> 1 },
713 relayport
=> { optional
=> 1 },
714 relaynomx
=> { optional
=> 1 },
715 dwarning
=> { optional
=> 1 },
716 max_smtpd_in
=> { optional
=> 1 },
717 max_smtpd_out
=> { optional
=> 1 },
718 greylist
=> { optional
=> 1 },
719 greylistmask4
=> { optional
=> 1 },
720 greylist6
=> { optional
=> 1 },
721 greylistmask6
=> { optional
=> 1 },
722 helotests
=> { optional
=> 1 },
723 tls
=> { optional
=> 1 },
724 tlslog
=> { optional
=> 1 },
725 tlsheader
=> { optional
=> 1 },
726 spf
=> { optional
=> 1 },
727 maxsize
=> { optional
=> 1 },
728 banner
=> { optional
=> 1 },
729 max_filters
=> { optional
=> 1 },
730 max_policy
=> { optional
=> 1 },
731 hide_received
=> { optional
=> 1 },
732 rejectunknown
=> { optional
=> 1 },
733 rejectunknownsender
=> { optional
=> 1 },
734 conn_count_limit
=> { optional
=> 1 },
735 conn_rate_limit
=> { optional
=> 1 },
736 message_rate_limit
=> { optional
=> 1 },
737 verifyreceivers
=> { optional
=> 1 },
738 dnsbl_sites
=> { optional
=> 1 },
739 dnsbl_threshold
=> { optional
=> 1 },
740 before_queue_filtering
=> { optional
=> 1 },
741 ndr_on_block
=> { optional
=> 1 },
742 smtputf8
=> { optional
=> 1 },
755 use PVE
::Tools
qw($IPV4RE $IPV6RE);
762 PMG
::Config
::Admin-
>register();
763 PMG
::Config
::Mail-
>register();
764 PMG
::Config
::SpamQuarantine-
>register();
765 PMG
::Config
::VirusQuarantine-
>register();
766 PMG
::Config
::Spam-
>register();
767 PMG
::Config
::ClamAV-
>register();
769 # initialize all plugins
770 PMG
::Config
::Base-
>init();
772 PVE
::JSONSchema
::register_format
(
773 'transport-domain', \
&pmg_verify_transport_domain
);
775 sub pmg_verify_transport_domain
{
776 my ($name, $noerr) = @_;
778 # like dns-name, but can contain leading dot
779 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
781 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
782 return undef if $noerr;
783 die "value does not look like a valid transport domain\n";
788 PVE
::JSONSchema
::register_format
(
789 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
791 sub pmg_verify_transport_domain_or_email
{
792 my ($name, $noerr) = @_;
794 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
797 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
801 # like dns-name, but can contain leading dot
802 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
803 return undef if $noerr;
804 die "value does not look like a valid transport domain or email address\n";
809 PVE
::JSONSchema
::register_format
(
810 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
812 sub pmg_verify_dnsbl_entry
{
813 my ($name, $noerr) = @_;
815 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
816 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
817 # we don't implement the ';' separated numbers in pattern, because this
818 # breaks at PVE::JSONSchema::split_list
819 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
821 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
822 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
823 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
824 return undef if $noerr;
825 die "value '$name' does not look like a valid dnsbl entry\n";
833 my $class = ref($type) || $type;
835 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
837 return bless $cfg, $class;
843 PVE
::INotify
::write_file
("pmg.conf", $self);
846 my $lockfile = "/var/lock/pmgconfig.lck";
849 my ($code, $errmsg) = @_;
851 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
853 $errmsg ?
die "$errmsg: $err" : die $err;
859 my ($self, $section, $key, $value) = @_;
861 my $pdata = PMG
::Config
::Base-
>private();
863 my $plugin = $pdata->{plugins
}->{$section};
864 die "no such section '$section'" if !$plugin;
866 if (defined($value)) {
867 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
868 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
869 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
871 if (defined($self->{ids
}->{$section})) {
872 delete $self->{ids
}->{$section}->{$key};
879 # get section value or default
881 my ($self, $section, $key, $nodefault) = @_;
883 my $pdata = PMG
::Config
::Base-
>private();
884 my $pdesc = $pdata->{propertyList
}->{$key};
885 die "no such property '$section/$key'\n"
886 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
887 defined($pdata->{options
}->{$section}->{$key}));
889 if (defined($self->{ids
}->{$section}) &&
890 defined(my $value = $self->{ids
}->{$section}->{$key})) {
894 return undef if $nodefault;
896 return $pdesc->{default};
899 # get a whole section with default value
901 my ($self, $section) = @_;
903 my $pdata = PMG
::Config
::Base-
>private();
904 return undef if !defined($pdata->{options
}->{$section});
908 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
910 my $pdesc = $pdata->{propertyList
}->{$key};
912 if (defined($self->{ids
}->{$section}) &&
913 defined(my $value = $self->{ids
}->{$section}->{$key})) {
914 $res->{$key} = $value;
917 $res->{$key} = $pdesc->{default};
923 # get a whole config with default values
927 my $pdata = PMG
::Config
::Base-
>private();
931 foreach my $type (keys %{$pdata->{plugins
}}) {
932 my $plugin = $pdata->{plugins
}->{$type};
933 $res->{$type} = $self->get_section($type);
940 my ($filename, $fh) = @_;
942 local $/ = undef; # slurp mode
945 $raw = <$fh> if defined($fh);
947 return PMG
::Config
::Base-
>parse_config($filename, $raw);
951 my ($filename, $fh, $cfg) = @_;
953 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
955 PVE
::Tools
::safe_print
($filename, $fh, $raw);
958 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
961 undef, always_call_parser
=> 1);
963 # parsers/writers for other files
965 my $domainsfilename = "/etc/pmg/domains";
967 sub postmap_pmg_domains
{
968 PMG
::Utils
::run_postmap
($domainsfilename);
971 sub read_pmg_domains
{
972 my ($filename, $fh) = @_;
978 while (defined(my $line = <$fh>)) {
980 next if $line =~ m/^\s*$/;
981 if ($line =~ m/^#(.*)\s*$/) {
985 if ($line =~ m/^(\S+)\s.*$/) {
987 $domains->{$domain} = {
988 domain
=> $domain, comment
=> $comment };
991 warn "parse error in '$filename': $line\n";
1000 sub write_pmg_domains
{
1001 my ($filename, $fh, $domains) = @_;
1003 foreach my $domain (sort keys %$domains) {
1004 my $comment = $domains->{$domain}->{comment
};
1005 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1006 if defined($comment) && $comment !~ m/^\s*$/;
1008 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
1012 PVE
::INotify
::register_file
('domains', $domainsfilename,
1014 \
&write_pmg_domains
,
1015 undef, always_call_parser
=> 1);
1017 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
1019 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
1021 \
&write_pmg_domains
,
1022 undef, always_call_parser
=> 1);
1024 my $mynetworks_filename = "/etc/pmg/mynetworks";
1026 sub read_pmg_mynetworks
{
1027 my ($filename, $fh) = @_;
1029 my $mynetworks = {};
1033 while (defined(my $line = <$fh>)) {
1035 next if $line =~ m/^\s*$/;
1036 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1037 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1038 my $cidr = "$network/${prefix_size}";
1039 # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
1040 $mynetworks->{$cidr} = {
1042 network_address
=> $network,
1043 prefix_size
=> $prefix_size,
1044 comment
=> $comment // '',
1047 warn "parse error in '$filename': $line\n";
1055 sub write_pmg_mynetworks
{
1056 my ($filename, $fh, $mynetworks) = @_;
1058 foreach my $cidr (sort keys %$mynetworks) {
1059 my $data = $mynetworks->{$cidr};
1060 my $comment = $data->{comment
} // '*';
1061 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1065 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1066 \
&read_pmg_mynetworks
,
1067 \
&write_pmg_mynetworks
,
1068 undef, always_call_parser
=> 1);
1070 PVE
::JSONSchema
::register_format
(
1071 'tls-policy', \
&pmg_verify_tls_policy
);
1073 # TODO: extend to parse attributes of the policy
1074 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1075 sub pmg_verify_tls_policy
{
1076 my ($policy, $noerr) = @_;
1078 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1079 return undef if $noerr;
1080 die "value '$policy' does not look like a valid tls policy\n";
1085 PVE
::JSONSchema
::register_format
(
1086 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1088 sub pmg_verify_tls_policy_strict
{
1089 my ($policy, $noerr) = @_;
1091 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1092 return undef if $noerr;
1093 die "value '$policy' does not look like a valid tls policy\n";
1098 PVE
::JSONSchema
::register_format
(
1099 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1101 sub pmg_verify_transport_domain_or_nexthop
{
1102 my ($name, $noerr) = @_;
1104 if (pmg_verify_transport_domain
($name, 1)) {
1106 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1108 if ($nexthop =~ m/^\[(.*)\]$/) {
1111 return $name if pmg_verify_transport_address
($nexthop, 1);
1113 return undef if $noerr;
1114 die "value does not look like a valid domain or next-hop\n";
1118 sub read_tls_policy
{
1119 my ($filename, $fh) = @_;
1121 return {} if !defined($fh);
1123 my $tls_policy = {};
1125 while (defined(my $line = <$fh>)) {
1127 next if $line =~ m/^\s*$/;
1128 next if $line =~ m/^#(.*)\s*$/;
1130 my $parse_error = sub {
1132 warn "parse error in '$filename': $line - $err\n";
1135 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1136 my ($destination, $policy) = ($1, $2);
1139 pmg_verify_transport_domain_or_nexthop
($destination);
1140 pmg_verify_tls_policy
($policy);
1143 $parse_error->($err);
1147 $tls_policy->{$destination} = {
1148 destination
=> $destination,
1152 $parse_error->('wrong format');
1159 sub write_tls_policy
{
1160 my ($filename, $fh, $tls_policy) = @_;
1162 return if !$tls_policy;
1164 foreach my $destination (sort keys %$tls_policy) {
1165 my $entry = $tls_policy->{$destination};
1166 PVE
::Tools
::safe_print
(
1167 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1171 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1172 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1175 undef, always_call_parser
=> 1);
1177 sub postmap_tls_policy
{
1178 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1181 sub read_tls_inbound_domains
{
1182 my ($filename, $fh) = @_;
1184 return {} if !defined($fh);
1188 while (defined(my $line = <$fh>)) {
1190 next if $line =~ m/^\s*$/;
1191 next if $line =~ m/^#(.*)\s*$/;
1193 my $parse_error = sub {
1195 warn "parse error in '$filename': $line - $err\n";
1198 if ($line =~ m/^(\S+) reject_plaintext_session$/) {
1201 eval { pmg_verify_transport_domain
($domain) };
1203 $parse_error->($err);
1207 $domains->{$domain} = 1;
1209 $parse_error->('wrong format');
1216 sub write_tls_inbound_domains
{
1217 my ($filename, $fh, $domains) = @_;
1219 return if !$domains;
1221 foreach my $domain (sort keys %$domains) {
1222 PVE
::Tools
::safe_print
($filename, $fh, "$domain reject_plaintext_session\n");
1226 my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
1227 PVE
::INotify
::register_file
('tls_inbound_domains', $tls_inbound_domains_map_filename,
1228 \
&read_tls_inbound_domains
,
1229 \
&write_tls_inbound_domains
,
1230 undef, always_call_parser
=> 1);
1232 sub postmap_tls_inbound_domains
{
1233 PMG
::Utils
::run_postmap
($tls_inbound_domains_map_filename);
1236 my $transport_map_filename = "/etc/pmg/transport";
1238 sub postmap_pmg_transport
{
1239 PMG
::Utils
::run_postmap
($transport_map_filename);
1242 PVE
::JSONSchema
::register_format
(
1243 'transport-address', \
&pmg_verify_transport_address
);
1245 sub pmg_verify_transport_address
{
1246 my ($name, $noerr) = @_;
1248 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1250 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1253 return undef if $noerr;
1254 die "value does not look like a valid address\n";
1258 sub read_transport_map
{
1259 my ($filename, $fh) = @_;
1261 return [] if !defined($fh);
1267 while (defined(my $line = <$fh>)) {
1269 next if $line =~ m/^\s*$/;
1270 if ($line =~ m/^#(.*)\s*$/) {
1275 my $parse_error = sub {
1277 warn "parse error in '$filename': $line - $err";
1281 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1282 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1284 eval { pmg_verify_transport_domain_or_email
($domain); };
1286 $parse_error->($err);
1290 if ($host =~ m/^\[(.*)\]$/) {
1294 $use_mx = 0 if ($protocol eq "lmtp");
1296 eval { pmg_verify_transport_address
($host); };
1298 $parse_error->($err);
1304 protocol
=> $protocol,
1308 comment
=> $comment,
1310 $res->{$domain} = $data;
1313 $parse_error->('wrong format');
1320 sub write_transport_map
{
1321 my ($filename, $fh, $tmap) = @_;
1325 foreach my $domain (sort keys %$tmap) {
1326 my $data = $tmap->{$domain};
1328 my $comment = $data->{comment
};
1329 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1330 if defined($comment) && $comment !~ m/^\s*$/;
1332 my $bracket_host = !$data->{use_mx
};
1334 if ($data->{protocol
} eq 'lmtp') {
1336 $data->{protocol
} .= ":inet";
1338 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1339 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1341 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1345 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1346 \
&read_transport_map
,
1347 \
&write_transport_map
,
1348 undef, always_call_parser
=> 1);
1350 # config file generation using templates
1352 sub get_host_dns_info
{
1356 my $nodename = PVE
::INotify
::nodename
();
1358 $dnsinfo->{hostname
} = $nodename;
1359 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1361 my $domain = $resolv->{search
} // 'localdomain';
1362 # postfix will not parse a hostname with trailing '.'
1363 $domain =~ s/^(.*)\.$/$1/;
1364 $dnsinfo->{domain
} = $domain;
1366 $dnsinfo->{fqdn
} = "$nodename.$domain";
1371 sub get_template_vars
{
1374 my $vars = { pmg
=> $self->get_config() };
1376 my $dnsinfo = get_host_dns_info
();
1377 $vars->{dns
} = $dnsinfo;
1378 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1379 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1381 my $transportnets = {};
1387 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1388 foreach my $domain (keys %$tmap) {
1389 my $data = $tmap->{$domain};
1390 my $host = $data->{host
};
1391 if ($host =~ m/^$IPV4RE$/) {
1392 $transportnets->{"$host/32"} = 1;
1393 $mynetworks->{"$host/32"} = 1;
1394 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1395 $transportnets->{"[$1]/128"} = 1;
1396 $mynetworks->{"[$1]/128"} = 1;
1401 $vars->{postfix
}->{transportnets
} = join(' ', sort keys %$transportnets);
1403 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1404 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1405 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1406 $mynetworks->{"[$1]/$2"} = 1;
1408 $mynetworks->{$int_net_cidr} = 1;
1411 if ($int_ip =~ m/^$IPV6RE$/) {
1412 $mynetworks->{"[$int_ip]/128"} = 1;
1414 $mynetworks->{"$int_ip/32"} = 1;
1419 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1420 foreach my $cidr (keys %$netlist) {
1421 my $ip = PVE
::Network
::IP_from_cidr
($cidr);
1424 warn "failed to parse mynetworks entry '$cidr', ignoring\n";
1425 } elsif ($ip->version() == 4) {
1426 $mynetworks->{$ip->prefix()} = 1;
1428 my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
1429 $mynetworks->{$address} = 1;
1433 # add default relay to mynetworks
1434 if (my $relay = $self->get('mail', 'relay')) {
1435 if ($relay =~ m/^$IPV4RE$/) {
1436 $mynetworks->{"$relay/32"} = 1;
1437 } elsif ($relay =~ m/^$IPV6RE$/) {
1438 $mynetworks->{"[$relay]/128"} = 1;
1440 # DNS name - do nothing ?
1444 $vars->{postfix
}->{mynetworks
} = join(' ', sort keys %$mynetworks);
1446 # normalize dnsbl_sites
1447 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1448 if (scalar(@dnsbl_sites)) {
1449 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1452 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1455 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1456 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1457 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1459 if (!defined($int_ip)) {
1460 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1461 $vars->{postfix
}->{int_ip
} = '127.0.0.1';
1462 } elsif ($int_ip =~ m/^$IPV6RE$/) {
1463 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1465 $vars->{postfix
}->{int_ip
} = $int_ip;
1468 my $wlbr = $dnsinfo->{fqdn
};
1469 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1472 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1474 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1476 my $uri = URI-
>new($proxy);
1477 my $host = $uri->host;
1478 my $port = $uri->port // 8080;
1480 my $data = { host
=> $host, port
=> $port };
1481 if (my $ui = $uri->userinfo) {
1482 my ($username, $pw) = split(/:/, $ui, 2);
1483 $data->{username
} = $username;
1484 $data->{password
} = $pw if defined($pw);
1486 $vars->{proxy
} = $data;
1489 warn "parse http_proxy failed - $@" if $@;
1491 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1496 # reads the $filename and checks if it's equal as the $cmp string passed
1497 my sub file_content_equals_str
{
1498 my ($filename, $cmp) = @_;
1500 return if !-f
$filename;
1501 my $current = PVE
::Tools
::file_get_contents
($filename, 128*1024);
1502 return defined($current) && $current eq $cmp; # no change
1505 # use one global TT cache
1506 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1508 my $template_toolkit;
1510 sub get_template_toolkit
{
1512 return $template_toolkit if $template_toolkit;
1514 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1516 return $template_toolkit;
1519 # rewrite file from template
1520 # return true if file has changed
1521 sub rewrite_config_file
{
1522 my ($self, $tmplname, $dstfn) = @_;
1524 my $demo = $self->get('admin', 'demo');
1527 my $demosrc = "$tmplname.demo";
1528 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1531 my ($perm, $uid, $gid);
1533 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1534 # needed if file contains a HTTPProxyPasswort
1536 $uid = getpwnam('clamav');
1537 $gid = getgrnam('adm');
1541 my $tt = get_template_toolkit
();
1543 my $vars = $self->get_template_vars();
1547 $tt->process($tmplname, $vars, \
$output) || die $tt->error() . "\n";
1549 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1551 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1553 if (defined($uid) && defined($gid)) {
1554 chown($uid, $gid, $dstfn);
1560 # rewrite spam configuration
1561 sub rewrite_config_spam
{
1564 my $use_awl = $self->get('spam', 'use_awl');
1565 my $use_bayes = $self->get('spam', 'use_bayes');
1566 my $use_razor = $self->get('spam', 'use_razor');
1570 # delete AW and bayes databases if those features are disabled
1572 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1576 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1577 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1578 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1581 # make sure we have the custom SA files (else cluster sync fails)
1582 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1583 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1585 $changes = 1 if $self->rewrite_config_file(
1586 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1588 $changes = 1 if $self->rewrite_config_file(
1589 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1591 $changes = 1 if $self->rewrite_config_file(
1592 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1594 $changes = 1 if $self->rewrite_config_file(
1595 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1597 $changes = 1 if $self->rewrite_config_file(
1598 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
1600 $changes = 1 if $self->rewrite_config_file(
1601 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
1604 mkdir "/root/.razor";
1606 $changes = 1 if $self->rewrite_config_file(
1607 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1609 if (! -e
'/root/.razor/identity') {
1612 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1613 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1616 syslog
('info', "registering razor failed: $err") if $err;
1623 # rewrite ClamAV configuration
1624 sub rewrite_config_clam
{
1627 return $self->rewrite_config_file(
1628 'clamd.conf.in', '/etc/clamav/clamd.conf');
1631 sub rewrite_config_freshclam
{
1634 return $self->rewrite_config_file(
1635 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1638 sub rewrite_config_postgres
{
1641 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1642 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1646 $changes = 1 if $self->rewrite_config_file(
1647 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1649 $changes = 1 if $self->rewrite_config_file(
1650 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1655 # rewrite /root/.forward
1656 sub rewrite_dot_forward
{
1659 my $dstfn = '/root/.forward';
1661 my $email = $self->get('admin', 'email');
1664 if ($email && $email =~ m/\s*(\S+)\s*/) {
1667 # empty .forward does not forward mails (see man local)
1669 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1671 PVE
::Tools
::file_set_contents
($dstfn, $output);
1676 my $write_smtp_whitelist = sub {
1677 my ($filename, $data, $action) = @_;
1679 $action = 'OK' if !$action;
1682 foreach my $k (sort keys %$data) {
1683 $new .= "$k $action\n";
1685 return 0 if file_content_equals_str
($filename, $new); # no change -> nothing to do
1687 PVE
::Tools
::file_set_contents
($filename, $new);
1689 PMG
::Utils
::run_postmap
($filename);
1694 sub rewrite_postfix_whitelist
{
1695 my ($rulecache) = @_;
1697 # see man page for regexp_table for postfix regex table format
1699 # we use a hash to avoid duplicate entries in regex tables
1702 my $clientlist = {};
1704 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1705 my $oclass = ref($obj);
1706 if ($oclass eq 'PMG::RuleDB::Receiver') {
1707 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1708 $tolist->{"/^$addr\$/"} = 1;
1709 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1710 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1711 $tolist->{"/^.+\@$addr\$/"} = 1;
1712 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1713 my $addr = $obj->{address
};
1715 $tolist->{"/^$addr\$/"} = 1;
1719 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1720 my $oclass = ref($obj);
1721 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1722 if ($oclass eq 'PMG::RuleDB::EMail') {
1723 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1724 $fromlist->{"/^$addr\$/"} = 1;
1725 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1726 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1727 $fromlist->{"/^.+\@$addr\$/"} = 1;
1728 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1729 my $addr = $obj->{address
};
1731 $fromlist->{"/^$addr\$/"} = 1;
1732 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1733 $clientlist->{$obj->{address
}} = 1;
1734 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1735 $clientlist->{$obj->{address
}} = 1;
1739 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1740 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1741 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1742 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1745 # rewrite /etc/postfix/*
1746 sub rewrite_config_postfix
{
1747 my ($self, $rulecache) = @_;
1749 # make sure we have required files (else postfix start fails)
1750 IO
::File-
>new($transport_map_filename, 'a', 0644);
1754 if ($self->get('mail', 'tls')) {
1756 PMG
::Utils
::gen_proxmox_tls_cert
();
1758 syslog
('info', "generating certificate failed: $@") if $@;
1761 $changes = 1 if $self->rewrite_config_file(
1762 'main.cf.in', '/etc/postfix/main.cf');
1764 $changes = 1 if $self->rewrite_config_file(
1765 'master.cf.in', '/etc/postfix/master.cf');
1767 # make sure we have required files (else postfix start fails)
1768 # Note: postmap need a valid /etc/postfix/main.cf configuration
1769 postmap_pmg_domains
();
1770 postmap_pmg_transport
();
1771 postmap_tls_policy
();
1772 postmap_tls_inbound_domains
();
1774 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1776 # make sure aliases.db is up to date
1777 system('/usr/bin/newaliases');
1782 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1783 my $pmg_service_params = {
1792 dkim_sign_all_mail
=> 1,
1796 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1797 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1799 sub dump_smtp_filter_config
{
1804 foreach my $sec (sort keys %$pmg_service_params) {
1805 my $conf_sec = $self->{ids
}->{$sec} // {};
1806 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1807 $val = $conf_sec->{$key};
1808 $conf .= "$sec.$key:$val\n" if defined($val);
1815 sub compare_smtp_filter_config
{
1821 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1825 syslog
('warning', "reloading pmg-smtp-filter: $err");
1828 my $new = $self->dump_smtp_filter_config();
1829 $ret = 1 if $old ne $new;
1832 $self->write_smtp_filter_config() if $ret;
1837 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1839 sub write_smtp_filter_config
{
1842 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1843 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1844 $self->dump_smtp_filter_config());
1850 sub rewrite_config
{
1851 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1853 $force_restart = {} if ! $force_restart;
1855 my $log_restart = sub {
1856 syslog
('info', "configuration change detected for '$_[0]', restarting");
1859 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1860 $force_restart->{postfix
}) {
1861 $log_restart->('postfix');
1862 PMG
::Utils
::service_cmd
('postfix', 'reload');
1865 if ($self->rewrite_dot_forward() && $restart_services) {
1866 # no need to restart anything
1869 if ($self->rewrite_config_postgres() && $restart_services) {
1870 # do nothing (too many side effects)?
1871 # does not happen anyways, because config does not change.
1874 if (($self->rewrite_config_spam() && $restart_services) ||
1875 $force_restart->{spam
}) {
1876 $log_restart->('pmg-smtp-filter');
1877 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1880 if (($self->rewrite_config_clam() && $restart_services) ||
1881 $force_restart->{clam
}) {
1882 $log_restart->('clamav-daemon');
1883 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1886 if (($self->rewrite_config_freshclam() && $restart_services) ||
1887 $force_restart->{freshclam
}) {
1888 $log_restart->('clamav-freshclam');
1889 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1892 if (($self->compare_smtp_filter_config() && $restart_services) ||
1893 $force_restart->{spam
}) {
1894 syslog
('info', "scheduled reload for pmg-smtp-filter");
1895 PMG
::Utils
::reload_smtp_filter
();