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) = @_;
943 $raw = do { local $/ = undef; <$fh> } if defined($fh);
945 return PMG
::Config
::Base-
>parse_config($filename, $raw);
949 my ($filename, $fh, $cfg) = @_;
951 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
953 PVE
::Tools
::safe_print
($filename, $fh, $raw);
956 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
959 undef, always_call_parser
=> 1);
961 # parsers/writers for other files
963 my $domainsfilename = "/etc/pmg/domains";
965 sub postmap_pmg_domains
{
966 PMG
::Utils
::run_postmap
($domainsfilename);
969 sub read_pmg_domains
{
970 my ($filename, $fh) = @_;
976 while (defined(my $line = <$fh>)) {
978 next if $line =~ m/^\s*$/;
979 if ($line =~ m/^#(.*)\s*$/) {
983 if ($line =~ m/^(\S+)\s.*$/) {
985 $domains->{$domain} = {
986 domain
=> $domain, comment
=> $comment };
989 warn "parse error in '$filename': $line\n";
998 sub write_pmg_domains
{
999 my ($filename, $fh, $domains) = @_;
1001 foreach my $domain (sort keys %$domains) {
1002 my $comment = $domains->{$domain}->{comment
};
1003 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1004 if defined($comment) && $comment !~ m/^\s*$/;
1006 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
1010 PVE
::INotify
::register_file
('domains', $domainsfilename,
1012 \
&write_pmg_domains
,
1013 undef, always_call_parser
=> 1);
1015 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
1017 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
1019 \
&write_pmg_domains
,
1020 undef, always_call_parser
=> 1);
1022 my $mynetworks_filename = "/etc/pmg/mynetworks";
1024 sub read_pmg_mynetworks
{
1025 my ($filename, $fh) = @_;
1027 my $mynetworks = {};
1031 while (defined(my $line = <$fh>)) {
1033 next if $line =~ m/^\s*$/;
1034 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1035 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1036 my $cidr = "$network/${prefix_size}";
1037 # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
1038 $mynetworks->{$cidr} = {
1040 network_address
=> $network,
1041 prefix_size
=> $prefix_size,
1042 comment
=> $comment // '',
1045 warn "parse error in '$filename': $line\n";
1053 sub write_pmg_mynetworks
{
1054 my ($filename, $fh, $mynetworks) = @_;
1056 foreach my $cidr (sort keys %$mynetworks) {
1057 my $data = $mynetworks->{$cidr};
1058 my $comment = $data->{comment
} // '*';
1059 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1063 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1064 \
&read_pmg_mynetworks
,
1065 \
&write_pmg_mynetworks
,
1066 undef, always_call_parser
=> 1);
1068 PVE
::JSONSchema
::register_format
(
1069 'tls-policy', \
&pmg_verify_tls_policy
);
1071 # TODO: extend to parse attributes of the policy
1072 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1073 sub pmg_verify_tls_policy
{
1074 my ($policy, $noerr) = @_;
1076 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1077 return undef if $noerr;
1078 die "value '$policy' does not look like a valid tls policy\n";
1083 PVE
::JSONSchema
::register_format
(
1084 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1086 sub pmg_verify_tls_policy_strict
{
1087 my ($policy, $noerr) = @_;
1089 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1090 return undef if $noerr;
1091 die "value '$policy' does not look like a valid tls policy\n";
1096 PVE
::JSONSchema
::register_format
(
1097 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1099 sub pmg_verify_transport_domain_or_nexthop
{
1100 my ($name, $noerr) = @_;
1102 if (pmg_verify_transport_domain
($name, 1)) {
1104 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1106 if ($nexthop =~ m/^\[(.*)\]$/) {
1109 return $name if pmg_verify_transport_address
($nexthop, 1);
1111 return undef if $noerr;
1112 die "value does not look like a valid domain or next-hop\n";
1116 sub read_tls_policy
{
1117 my ($filename, $fh) = @_;
1119 return {} if !defined($fh);
1121 my $tls_policy = {};
1123 while (defined(my $line = <$fh>)) {
1125 next if $line =~ m/^\s*$/;
1126 next if $line =~ m/^#(.*)\s*$/;
1128 my $parse_error = sub {
1130 warn "parse error in '$filename': $line - $err\n";
1133 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1134 my ($destination, $policy) = ($1, $2);
1137 pmg_verify_transport_domain_or_nexthop
($destination);
1138 pmg_verify_tls_policy
($policy);
1141 $parse_error->($err);
1145 $tls_policy->{$destination} = {
1146 destination
=> $destination,
1150 $parse_error->('wrong format');
1157 sub write_tls_policy
{
1158 my ($filename, $fh, $tls_policy) = @_;
1160 return if !$tls_policy;
1162 foreach my $destination (sort keys %$tls_policy) {
1163 my $entry = $tls_policy->{$destination};
1164 PVE
::Tools
::safe_print
(
1165 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1169 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1170 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1173 undef, always_call_parser
=> 1);
1175 sub postmap_tls_policy
{
1176 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1179 sub read_tls_inbound_domains
{
1180 my ($filename, $fh) = @_;
1182 return {} if !defined($fh);
1186 while (defined(my $line = <$fh>)) {
1188 next if $line =~ m/^\s*$/;
1189 next if $line =~ m/^#(.*)\s*$/;
1191 my $parse_error = sub {
1193 warn "parse error in '$filename': $line - $err\n";
1196 if ($line =~ m/^(\S+) reject_plaintext_session$/) {
1199 eval { pmg_verify_transport_domain
($domain) };
1201 $parse_error->($err);
1205 $domains->{$domain} = 1;
1207 $parse_error->('wrong format');
1214 sub write_tls_inbound_domains
{
1215 my ($filename, $fh, $domains) = @_;
1217 return if !$domains;
1219 foreach my $domain (sort keys %$domains) {
1220 PVE
::Tools
::safe_print
($filename, $fh, "$domain reject_plaintext_session\n");
1224 my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
1225 PVE
::INotify
::register_file
('tls_inbound_domains', $tls_inbound_domains_map_filename,
1226 \
&read_tls_inbound_domains
,
1227 \
&write_tls_inbound_domains
,
1228 undef, always_call_parser
=> 1);
1230 sub postmap_tls_inbound_domains
{
1231 PMG
::Utils
::run_postmap
($tls_inbound_domains_map_filename);
1234 my $transport_map_filename = "/etc/pmg/transport";
1236 sub postmap_pmg_transport
{
1237 PMG
::Utils
::run_postmap
($transport_map_filename);
1240 PVE
::JSONSchema
::register_format
(
1241 'transport-address', \
&pmg_verify_transport_address
);
1243 sub pmg_verify_transport_address
{
1244 my ($name, $noerr) = @_;
1246 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1248 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1251 return undef if $noerr;
1252 die "value does not look like a valid address\n";
1256 sub read_transport_map
{
1257 my ($filename, $fh) = @_;
1259 return [] if !defined($fh);
1265 while (defined(my $line = <$fh>)) {
1267 next if $line =~ m/^\s*$/;
1268 if ($line =~ m/^#(.*)\s*$/) {
1273 my $parse_error = sub {
1275 warn "parse error in '$filename': $line - $err";
1279 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1280 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1282 eval { pmg_verify_transport_domain_or_email
($domain); };
1284 $parse_error->($err);
1288 if ($host =~ m/^\[(.*)\]$/) {
1292 $use_mx = 0 if ($protocol eq "lmtp");
1294 eval { pmg_verify_transport_address
($host); };
1296 $parse_error->($err);
1302 protocol
=> $protocol,
1306 comment
=> $comment,
1308 $res->{$domain} = $data;
1311 $parse_error->('wrong format');
1318 sub write_transport_map
{
1319 my ($filename, $fh, $tmap) = @_;
1323 foreach my $domain (sort keys %$tmap) {
1324 my $data = $tmap->{$domain};
1326 my $comment = $data->{comment
};
1327 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1328 if defined($comment) && $comment !~ m/^\s*$/;
1330 my $bracket_host = !$data->{use_mx
};
1332 if ($data->{protocol
} eq 'lmtp') {
1334 $data->{protocol
} .= ":inet";
1336 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1337 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1339 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1343 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1344 \
&read_transport_map
,
1345 \
&write_transport_map
,
1346 undef, always_call_parser
=> 1);
1348 # config file generation using templates
1350 sub get_host_dns_info
{
1354 my $nodename = PVE
::INotify
::nodename
();
1356 $dnsinfo->{hostname
} = $nodename;
1357 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1359 my $domain = $resolv->{search
} // 'localdomain';
1360 # postfix will not parse a hostname with trailing '.'
1361 $domain =~ s/^(.*)\.$/$1/;
1362 $dnsinfo->{domain
} = $domain;
1364 $dnsinfo->{fqdn
} = "$nodename.$domain";
1369 sub get_template_vars
{
1372 my $vars = { pmg
=> $self->get_config() };
1374 my $dnsinfo = get_host_dns_info
();
1375 $vars->{dns
} = $dnsinfo;
1376 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1377 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1379 my $transportnets = {};
1385 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1386 foreach my $domain (keys %$tmap) {
1387 my $data = $tmap->{$domain};
1388 my $host = $data->{host
};
1389 if ($host =~ m/^$IPV4RE$/) {
1390 $transportnets->{"$host/32"} = 1;
1391 $mynetworks->{"$host/32"} = 1;
1392 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1393 $transportnets->{"[$1]/128"} = 1;
1394 $mynetworks->{"[$1]/128"} = 1;
1399 $vars->{postfix
}->{transportnets
} = join(' ', sort keys %$transportnets);
1401 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1402 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1403 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1404 $mynetworks->{"[$1]/$2"} = 1;
1406 $mynetworks->{$int_net_cidr} = 1;
1409 if ($int_ip =~ m/^$IPV6RE$/) {
1410 $mynetworks->{"[$int_ip]/128"} = 1;
1412 $mynetworks->{"$int_ip/32"} = 1;
1417 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1418 foreach my $cidr (keys %$netlist) {
1419 my $ip = PVE
::Network
::IP_from_cidr
($cidr);
1422 warn "failed to parse mynetworks entry '$cidr', ignoring\n";
1423 } elsif ($ip->version() == 4) {
1424 $mynetworks->{$ip->prefix()} = 1;
1426 my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
1427 $mynetworks->{$address} = 1;
1431 # add default relay to mynetworks
1432 if (my $relay = $self->get('mail', 'relay')) {
1433 if ($relay =~ m/^$IPV4RE$/) {
1434 $mynetworks->{"$relay/32"} = 1;
1435 } elsif ($relay =~ m/^$IPV6RE$/) {
1436 $mynetworks->{"[$relay]/128"} = 1;
1438 # DNS name - do nothing ?
1442 $vars->{postfix
}->{mynetworks
} = join(' ', sort keys %$mynetworks);
1444 # normalize dnsbl_sites
1445 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1446 if (scalar(@dnsbl_sites)) {
1447 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1450 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1453 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1454 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1455 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1457 if (!defined($int_ip)) {
1458 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1459 $vars->{postfix
}->{int_ip
} = '127.0.0.1';
1460 } elsif ($int_ip =~ m/^$IPV6RE$/) {
1461 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1463 $vars->{postfix
}->{int_ip
} = $int_ip;
1466 my $wlbr = $dnsinfo->{fqdn
};
1467 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1470 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1472 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1474 my $uri = URI-
>new($proxy);
1475 my $host = $uri->host;
1476 my $port = $uri->port // 8080;
1478 my $data = { host
=> $host, port
=> $port };
1479 if (my $ui = $uri->userinfo) {
1480 my ($username, $pw) = split(/:/, $ui, 2);
1481 $data->{username
} = $username;
1482 $data->{password
} = $pw if defined($pw);
1484 $vars->{proxy
} = $data;
1487 warn "parse http_proxy failed - $@" if $@;
1489 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1494 # reads the $filename and checks if it's equal as the $cmp string passed
1495 my sub file_content_equals_str
{
1496 my ($filename, $cmp) = @_;
1498 return if !-f
$filename;
1499 my $current = PVE
::Tools
::file_get_contents
($filename, 128*1024);
1500 return defined($current) && $current eq $cmp; # no change
1503 # use one global TT cache
1504 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1506 my $template_toolkit;
1508 sub get_template_toolkit
{
1510 return $template_toolkit if $template_toolkit;
1512 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1514 return $template_toolkit;
1517 # rewrite file from template
1518 # return true if file has changed
1519 sub rewrite_config_file
{
1520 my ($self, $tmplname, $dstfn) = @_;
1522 my $demo = $self->get('admin', 'demo');
1525 my $demosrc = "$tmplname.demo";
1526 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1529 my ($perm, $uid, $gid);
1531 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1532 # needed if file contains a HTTPProxyPasswort
1534 $uid = getpwnam('clamav');
1535 $gid = getgrnam('adm');
1539 my $tt = get_template_toolkit
();
1541 my $vars = $self->get_template_vars();
1545 $tt->process($tmplname, $vars, \
$output) || die $tt->error() . "\n";
1547 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1549 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1551 if (defined($uid) && defined($gid)) {
1552 chown($uid, $gid, $dstfn);
1558 # rewrite spam configuration
1559 sub rewrite_config_spam
{
1562 my $use_awl = $self->get('spam', 'use_awl');
1563 my $use_bayes = $self->get('spam', 'use_bayes');
1564 my $use_razor = $self->get('spam', 'use_razor');
1568 # delete AW and bayes databases if those features are disabled
1570 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1574 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1575 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1576 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1579 # make sure we have the custom SA files (else cluster sync fails)
1580 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1581 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1583 $changes = 1 if $self->rewrite_config_file(
1584 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1586 $changes = 1 if $self->rewrite_config_file(
1587 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1589 $changes = 1 if $self->rewrite_config_file(
1590 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1592 $changes = 1 if $self->rewrite_config_file(
1593 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1595 $changes = 1 if $self->rewrite_config_file(
1596 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
1598 $changes = 1 if $self->rewrite_config_file(
1599 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
1602 mkdir "/root/.razor";
1604 $changes = 1 if $self->rewrite_config_file(
1605 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1607 if (! -e
'/root/.razor/identity') {
1610 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1611 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1614 syslog
('info', "registering razor failed: $err") if $err;
1621 # rewrite ClamAV configuration
1622 sub rewrite_config_clam
{
1625 return $self->rewrite_config_file(
1626 'clamd.conf.in', '/etc/clamav/clamd.conf');
1629 sub rewrite_config_freshclam
{
1632 return $self->rewrite_config_file(
1633 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1636 sub rewrite_config_postgres
{
1639 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1640 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1644 $changes = 1 if $self->rewrite_config_file(
1645 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1647 $changes = 1 if $self->rewrite_config_file(
1648 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1653 # rewrite /root/.forward
1654 sub rewrite_dot_forward
{
1657 my $dstfn = '/root/.forward';
1659 my $email = $self->get('admin', 'email');
1662 if ($email && $email =~ m/\s*(\S+)\s*/) {
1665 # empty .forward does not forward mails (see man local)
1667 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1669 PVE
::Tools
::file_set_contents
($dstfn, $output);
1674 my $write_smtp_whitelist = sub {
1675 my ($filename, $data, $action) = @_;
1677 $action = 'OK' if !$action;
1680 foreach my $k (sort keys %$data) {
1681 $new .= "$k $action\n";
1683 return 0 if file_content_equals_str
($filename, $new); # no change -> nothing to do
1685 PVE
::Tools
::file_set_contents
($filename, $new);
1687 PMG
::Utils
::run_postmap
($filename);
1692 sub rewrite_postfix_whitelist
{
1693 my ($rulecache) = @_;
1695 # see man page for regexp_table for postfix regex table format
1697 # we use a hash to avoid duplicate entries in regex tables
1700 my $clientlist = {};
1702 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1703 my $oclass = ref($obj);
1704 if ($oclass eq 'PMG::RuleDB::Receiver') {
1705 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1706 $tolist->{"/^$addr\$/"} = 1;
1707 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1708 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1709 $tolist->{"/^.+\@$addr\$/"} = 1;
1710 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1711 my $addr = $obj->{address
};
1713 $tolist->{"/^$addr\$/"} = 1;
1717 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1718 my $oclass = ref($obj);
1719 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1720 if ($oclass eq 'PMG::RuleDB::EMail') {
1721 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1722 $fromlist->{"/^$addr\$/"} = 1;
1723 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1724 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1725 $fromlist->{"/^.+\@$addr\$/"} = 1;
1726 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1727 my $addr = $obj->{address
};
1729 $fromlist->{"/^$addr\$/"} = 1;
1730 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1731 $clientlist->{$obj->{address
}} = 1;
1732 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1733 $clientlist->{$obj->{address
}} = 1;
1737 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1738 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1739 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1740 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1743 # rewrite /etc/postfix/*
1744 sub rewrite_config_postfix
{
1745 my ($self, $rulecache) = @_;
1747 # make sure we have required files (else postfix start fails)
1748 IO
::File-
>new($transport_map_filename, 'a', 0644);
1752 if ($self->get('mail', 'tls')) {
1754 PMG
::Utils
::gen_proxmox_tls_cert
();
1756 syslog
('info', "generating certificate failed: $@") if $@;
1759 $changes = 1 if $self->rewrite_config_file(
1760 'main.cf.in', '/etc/postfix/main.cf');
1762 $changes = 1 if $self->rewrite_config_file(
1763 'master.cf.in', '/etc/postfix/master.cf');
1765 # make sure we have required files (else postfix start fails)
1766 # Note: postmap need a valid /etc/postfix/main.cf configuration
1767 postmap_pmg_domains
();
1768 postmap_pmg_transport
();
1769 postmap_tls_policy
();
1770 postmap_tls_inbound_domains
();
1772 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1774 # make sure aliases.db is up to date
1775 system('/usr/bin/newaliases');
1780 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1781 my $pmg_service_params = {
1790 dkim_sign_all_mail
=> 1,
1794 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1795 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1797 sub dump_smtp_filter_config
{
1802 foreach my $sec (sort keys %$pmg_service_params) {
1803 my $conf_sec = $self->{ids
}->{$sec} // {};
1804 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1805 $val = $conf_sec->{$key};
1806 $conf .= "$sec.$key:$val\n" if defined($val);
1813 sub compare_smtp_filter_config
{
1819 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1823 syslog
('warning', "reloading pmg-smtp-filter: $err");
1826 my $new = $self->dump_smtp_filter_config();
1827 $ret = 1 if $old ne $new;
1830 $self->write_smtp_filter_config() if $ret;
1835 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1837 sub write_smtp_filter_config
{
1840 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1841 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1842 $self->dump_smtp_filter_config());
1848 sub rewrite_config
{
1849 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1851 $force_restart = {} if ! $force_restart;
1853 my $log_restart = sub {
1854 syslog
('info', "configuration change detected for '$_[0]', restarting");
1857 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1858 $force_restart->{postfix
}) {
1859 $log_restart->('postfix');
1860 PMG
::Utils
::service_cmd
('postfix', 'reload');
1863 if ($self->rewrite_dot_forward() && $restart_services) {
1864 # no need to restart anything
1867 if ($self->rewrite_config_postgres() && $restart_services) {
1868 # do nothing (too many side effects)?
1869 # does not happen anyways, because config does not change.
1872 if (($self->rewrite_config_spam() && $restart_services) ||
1873 $force_restart->{spam
}) {
1874 $log_restart->('pmg-smtp-filter');
1875 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1878 if (($self->rewrite_config_clam() && $restart_services) ||
1879 $force_restart->{clam
}) {
1880 $log_restart->('clamav-daemon');
1881 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1884 if (($self->rewrite_config_freshclam() && $restart_services) ||
1885 $force_restart->{freshclam
}) {
1886 $log_restart->('clamav-freshclam');
1887 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1890 if (($self->compare_smtp_filter_config() && $restart_services) ||
1891 $force_restart->{spam
}) {
1892 syslog
('info', "scheduled reload for pmg-smtp-filter");
1893 PMG
::Utils
::reload_smtp_filter
();