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
463 my $memory = physical_memory
();
464 if ($memory < 3840) {
465 warn "low amount of system memory installed, recommended is 4+ GB\n"
466 ."to prevent OOM kills, it is better to set max_filters manually\n";
467 $base = $memory > 1536 ?
1024 : 512;
472 my $add_servers = int(($memory - $base)/$servermem);
473 $max_servers += $add_servers if $add_servers > 0;
474 $max_servers = 40 if $max_servers > 40;
476 return $max_servers - 2;
480 # estimate optimal number of smtpd daemons
482 my $max_servers = 25;
484 my $memory = physical_memory
();
485 my $add_servers = int(($memory - 512)/$servermem);
486 $max_servers += $add_servers if $add_servers > 0;
487 $max_servers = 100 if $max_servers > 100;
492 # estimate optimal number of proxpolicy servers
494 my $memory = physical_memory
();
495 $max_servers = 5 if $memory >= 500;
502 description
=> "SMTP port number for outgoing mail (trusted).",
509 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
516 description
=> "The default mail delivery transport (incoming mails).",
517 type
=> 'string', format
=> 'address',
520 description
=> "Transport protocol for relay host.",
522 enum
=> [qw(smtp lmtp)],
526 description
=> "SMTP/LMTP port number for relay host.",
533 description
=> "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
538 description
=> "When set, all outgoing mails are deliverd to the specified smarthost."
539 ." (postfix option `default_transport`)",
540 type
=> 'string', format
=> 'address',
543 description
=> "SMTP port number for smarthost. (postfix option `default_transport`)",
550 description
=> "ESMTP banner.",
553 default => 'ESMTP Proxmox',
556 description
=> "Maximum number of pmg-smtp-filter processes.",
560 default => get_max_filters
(),
563 description
=> "Maximum number of pmgpolicy processes.",
567 default => get_max_policy
(),
570 description
=> "Maximum number of SMTP daemon processes (in).",
574 default => get_max_smtpd
(),
577 description
=> "Maximum number of SMTP daemon processes (out).",
581 default => get_max_smtpd
(),
583 conn_count_limit
=> {
584 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
590 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.",
595 message_rate_limit
=> {
596 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.",
602 description
=> "Hide received header in outgoing mails.",
607 description
=> "Maximum email size. Larger mails are rejected. (postfix option `message_size_limit`)",
610 default => 1024*1024*10,
613 description
=> "SMTP delay warning time (in hours). (postfix option `delay_warning_time`)",
619 description
=> "Enable TLS.",
624 description
=> "Enable TLS Logging.",
629 description
=> "Add TLS received header.",
634 description
=> "Use Sender Policy Framework.",
639 description
=> "Use Greylisting for IPv4.",
644 description
=> "Netmask to apply for greylisting IPv4 hosts",
651 description
=> "Use Greylisting for IPv6.",
656 description
=> "Netmask to apply for greylisting IPv6 hosts",
663 description
=> "Use SMTP HELO tests. (postfix option `smtpd_helo_restrictions`)",
668 description
=> "Reject unknown clients. (postfix option `reject_unknown_client_hostname`)",
672 rejectunknownsender
=> {
673 description
=> "Reject unknown senders. (postfix option `reject_unknown_sender_domain`)",
678 description
=> "Enable receiver verification. The value spefifies the numerical reply"
679 ." code when the Postfix SMTP server rejects a recipient address."
680 ." (postfix options `reject_unknown_recipient_domain`, `reject_unverified_recipient`,"
681 ." and `unverified_recipient_reject_code`)",
683 enum
=> ['450', '550'],
686 description
=> "Optional list of DNS white/blacklist domains (postfix option `postscreen_dnsbl_sites`).",
687 type
=> 'string', format
=> 'dnsbl-entry-list',
690 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on"
691 ." its combined DNSBL score (postfix option `postscreen_dnsbl_threshold`).",
696 before_queue_filtering
=> {
697 description
=> "Enable before queue filtering by pmg-smtp-filter",
702 description
=> "Send out NDR when mail gets blocked",
707 description
=> "Enable SMTPUTF8 support in Postfix and detection for locally generated mail (postfix option `smtputf8_enable`)",
716 int_port
=> { optional
=> 1 },
717 ext_port
=> { optional
=> 1 },
718 smarthost
=> { optional
=> 1 },
719 smarthostport
=> { optional
=> 1 },
720 relay
=> { optional
=> 1 },
721 relayprotocol
=> { optional
=> 1 },
722 relayport
=> { optional
=> 1 },
723 relaynomx
=> { optional
=> 1 },
724 dwarning
=> { optional
=> 1 },
725 max_smtpd_in
=> { optional
=> 1 },
726 max_smtpd_out
=> { optional
=> 1 },
727 greylist
=> { optional
=> 1 },
728 greylistmask4
=> { optional
=> 1 },
729 greylist6
=> { optional
=> 1 },
730 greylistmask6
=> { optional
=> 1 },
731 helotests
=> { optional
=> 1 },
732 tls
=> { optional
=> 1 },
733 tlslog
=> { optional
=> 1 },
734 tlsheader
=> { optional
=> 1 },
735 spf
=> { optional
=> 1 },
736 maxsize
=> { optional
=> 1 },
737 banner
=> { optional
=> 1 },
738 max_filters
=> { optional
=> 1 },
739 max_policy
=> { optional
=> 1 },
740 hide_received
=> { optional
=> 1 },
741 rejectunknown
=> { optional
=> 1 },
742 rejectunknownsender
=> { optional
=> 1 },
743 conn_count_limit
=> { optional
=> 1 },
744 conn_rate_limit
=> { optional
=> 1 },
745 message_rate_limit
=> { optional
=> 1 },
746 verifyreceivers
=> { optional
=> 1 },
747 dnsbl_sites
=> { optional
=> 1 },
748 dnsbl_threshold
=> { optional
=> 1 },
749 before_queue_filtering
=> { optional
=> 1 },
750 ndr_on_block
=> { optional
=> 1 },
751 smtputf8
=> { optional
=> 1 },
764 use PVE
::Tools
qw($IPV4RE $IPV6RE);
771 PMG
::Config
::Admin-
>register();
772 PMG
::Config
::Mail-
>register();
773 PMG
::Config
::SpamQuarantine-
>register();
774 PMG
::Config
::VirusQuarantine-
>register();
775 PMG
::Config
::Spam-
>register();
776 PMG
::Config
::ClamAV-
>register();
778 # initialize all plugins
779 PMG
::Config
::Base-
>init();
781 PVE
::JSONSchema
::register_format
(
782 'transport-domain', \
&pmg_verify_transport_domain
);
784 sub pmg_verify_transport_domain
{
785 my ($name, $noerr) = @_;
787 # like dns-name, but can contain leading dot
788 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
790 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
791 return undef if $noerr;
792 die "value does not look like a valid transport domain\n";
797 PVE
::JSONSchema
::register_format
(
798 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
800 sub pmg_verify_transport_domain_or_email
{
801 my ($name, $noerr) = @_;
803 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
806 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
810 # like dns-name, but can contain leading dot
811 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
812 return undef if $noerr;
813 die "value does not look like a valid transport domain or email address\n";
818 PVE
::JSONSchema
::register_format
(
819 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
821 sub pmg_verify_dnsbl_entry
{
822 my ($name, $noerr) = @_;
824 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
825 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
826 # we don't implement the ';' separated numbers in pattern, because this
827 # breaks at PVE::JSONSchema::split_list
828 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
830 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
831 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
832 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
833 return undef if $noerr;
834 die "value '$name' does not look like a valid dnsbl entry\n";
842 my $class = ref($type) || $type;
844 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
846 return bless $cfg, $class;
852 PVE
::INotify
::write_file
("pmg.conf", $self);
855 my $lockfile = "/var/lock/pmgconfig.lck";
858 my ($code, $errmsg) = @_;
860 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
862 $errmsg ?
die "$errmsg: $err" : die $err;
868 my ($self, $section, $key, $value) = @_;
870 my $pdata = PMG
::Config
::Base-
>private();
872 my $plugin = $pdata->{plugins
}->{$section};
873 die "no such section '$section'" if !$plugin;
875 if (defined($value)) {
876 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
877 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
878 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
880 if (defined($self->{ids
}->{$section})) {
881 delete $self->{ids
}->{$section}->{$key};
888 # get section value or default
890 my ($self, $section, $key, $nodefault) = @_;
892 my $pdata = PMG
::Config
::Base-
>private();
893 my $pdesc = $pdata->{propertyList
}->{$key};
894 die "no such property '$section/$key'\n"
895 if !(defined($pdesc) && defined($pdata->{options
}->{$section}) &&
896 defined($pdata->{options
}->{$section}->{$key}));
898 if (defined($self->{ids
}->{$section}) &&
899 defined(my $value = $self->{ids
}->{$section}->{$key})) {
903 return undef if $nodefault;
905 return $pdesc->{default};
908 # get a whole section with default value
910 my ($self, $section) = @_;
912 my $pdata = PMG
::Config
::Base-
>private();
913 return undef if !defined($pdata->{options
}->{$section});
917 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
919 my $pdesc = $pdata->{propertyList
}->{$key};
921 if (defined($self->{ids
}->{$section}) &&
922 defined(my $value = $self->{ids
}->{$section}->{$key})) {
923 $res->{$key} = $value;
926 $res->{$key} = $pdesc->{default};
932 # get a whole config with default values
936 my $pdata = PMG
::Config
::Base-
>private();
940 foreach my $type (keys %{$pdata->{plugins
}}) {
941 my $plugin = $pdata->{plugins
}->{$type};
942 $res->{$type} = $self->get_section($type);
949 my ($filename, $fh) = @_;
952 $raw = do { local $/ = undef; <$fh> } if defined($fh);
954 return PMG
::Config
::Base-
>parse_config($filename, $raw);
958 my ($filename, $fh, $cfg) = @_;
960 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
962 PVE
::Tools
::safe_print
($filename, $fh, $raw);
965 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
968 undef, always_call_parser
=> 1);
970 # parsers/writers for other files
972 my $domainsfilename = "/etc/pmg/domains";
974 sub postmap_pmg_domains
{
975 PMG
::Utils
::run_postmap
($domainsfilename);
978 sub read_pmg_domains
{
979 my ($filename, $fh) = @_;
985 while (defined(my $line = <$fh>)) {
987 next if $line =~ m/^\s*$/;
988 if ($line =~ m/^#(.*)\s*$/) {
992 if ($line =~ m/^(\S+)\s.*$/) {
994 $domains->{$domain} = {
995 domain
=> $domain, comment
=> $comment };
998 warn "parse error in '$filename': $line\n";
1007 sub write_pmg_domains
{
1008 my ($filename, $fh, $domains) = @_;
1010 foreach my $domain (sort keys %$domains) {
1011 my $comment = $domains->{$domain}->{comment
};
1012 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1013 if defined($comment) && $comment !~ m/^\s*$/;
1015 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
1019 PVE
::INotify
::register_file
('domains', $domainsfilename,
1021 \
&write_pmg_domains
,
1022 undef, always_call_parser
=> 1);
1024 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
1026 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
1028 \
&write_pmg_domains
,
1029 undef, always_call_parser
=> 1);
1031 my $mynetworks_filename = "/etc/pmg/mynetworks";
1033 sub read_pmg_mynetworks
{
1034 my ($filename, $fh) = @_;
1036 my $mynetworks = {};
1040 while (defined(my $line = <$fh>)) {
1042 next if $line =~ m/^\s*$/;
1043 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1044 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1045 my $cidr = "$network/${prefix_size}";
1046 # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
1047 $mynetworks->{$cidr} = {
1049 network_address
=> $network,
1050 prefix_size
=> $prefix_size,
1051 comment
=> $comment // '',
1054 warn "parse error in '$filename': $line\n";
1062 sub write_pmg_mynetworks
{
1063 my ($filename, $fh, $mynetworks) = @_;
1065 foreach my $cidr (sort keys %$mynetworks) {
1066 my $data = $mynetworks->{$cidr};
1067 my $comment = $data->{comment
} // '*';
1068 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1072 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1073 \
&read_pmg_mynetworks
,
1074 \
&write_pmg_mynetworks
,
1075 undef, always_call_parser
=> 1);
1077 PVE
::JSONSchema
::register_format
(
1078 'tls-policy', \
&pmg_verify_tls_policy
);
1080 # TODO: extend to parse attributes of the policy
1081 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1082 sub pmg_verify_tls_policy
{
1083 my ($policy, $noerr) = @_;
1085 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1086 return undef if $noerr;
1087 die "value '$policy' does not look like a valid tls policy\n";
1092 PVE
::JSONSchema
::register_format
(
1093 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1095 sub pmg_verify_tls_policy_strict
{
1096 my ($policy, $noerr) = @_;
1098 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1099 return undef if $noerr;
1100 die "value '$policy' does not look like a valid tls policy\n";
1105 PVE
::JSONSchema
::register_format
(
1106 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1108 sub pmg_verify_transport_domain_or_nexthop
{
1109 my ($name, $noerr) = @_;
1111 if (pmg_verify_transport_domain
($name, 1)) {
1113 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1115 if ($nexthop =~ m/^\[(.*)\]$/) {
1118 return $name if pmg_verify_transport_address
($nexthop, 1);
1120 return undef if $noerr;
1121 die "value does not look like a valid domain or next-hop\n";
1125 sub read_tls_policy
{
1126 my ($filename, $fh) = @_;
1128 return {} if !defined($fh);
1130 my $tls_policy = {};
1132 while (defined(my $line = <$fh>)) {
1134 next if $line =~ m/^\s*$/;
1135 next if $line =~ m/^#(.*)\s*$/;
1137 my $parse_error = sub {
1139 warn "parse error in '$filename': $line - $err\n";
1142 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1143 my ($destination, $policy) = ($1, $2);
1146 pmg_verify_transport_domain_or_nexthop
($destination);
1147 pmg_verify_tls_policy
($policy);
1150 $parse_error->($err);
1154 $tls_policy->{$destination} = {
1155 destination
=> $destination,
1159 $parse_error->('wrong format');
1166 sub write_tls_policy
{
1167 my ($filename, $fh, $tls_policy) = @_;
1169 return if !$tls_policy;
1171 foreach my $destination (sort keys %$tls_policy) {
1172 my $entry = $tls_policy->{$destination};
1173 PVE
::Tools
::safe_print
(
1174 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1178 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1179 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1182 undef, always_call_parser
=> 1);
1184 sub postmap_tls_policy
{
1185 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1188 sub read_tls_inbound_domains
{
1189 my ($filename, $fh) = @_;
1191 return {} if !defined($fh);
1195 while (defined(my $line = <$fh>)) {
1197 next if $line =~ m/^\s*$/;
1198 next if $line =~ m/^#(.*)\s*$/;
1200 my $parse_error = sub {
1202 warn "parse error in '$filename': $line - $err\n";
1205 if ($line =~ m/^(\S+) reject_plaintext_session$/) {
1208 eval { pmg_verify_transport_domain
($domain) };
1210 $parse_error->($err);
1214 $domains->{$domain} = 1;
1216 $parse_error->('wrong format');
1223 sub write_tls_inbound_domains
{
1224 my ($filename, $fh, $domains) = @_;
1226 return if !$domains;
1228 foreach my $domain (sort keys %$domains) {
1229 PVE
::Tools
::safe_print
($filename, $fh, "$domain reject_plaintext_session\n");
1233 my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
1234 PVE
::INotify
::register_file
('tls_inbound_domains', $tls_inbound_domains_map_filename,
1235 \
&read_tls_inbound_domains
,
1236 \
&write_tls_inbound_domains
,
1237 undef, always_call_parser
=> 1);
1239 sub postmap_tls_inbound_domains
{
1240 PMG
::Utils
::run_postmap
($tls_inbound_domains_map_filename);
1243 my $transport_map_filename = "/etc/pmg/transport";
1245 sub postmap_pmg_transport
{
1246 PMG
::Utils
::run_postmap
($transport_map_filename);
1249 PVE
::JSONSchema
::register_format
(
1250 'transport-address', \
&pmg_verify_transport_address
);
1252 sub pmg_verify_transport_address
{
1253 my ($name, $noerr) = @_;
1255 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1257 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1260 return undef if $noerr;
1261 die "value does not look like a valid address\n";
1265 sub read_transport_map
{
1266 my ($filename, $fh) = @_;
1268 return [] if !defined($fh);
1274 while (defined(my $line = <$fh>)) {
1276 next if $line =~ m/^\s*$/;
1277 if ($line =~ m/^#(.*)\s*$/) {
1282 my $parse_error = sub {
1284 warn "parse error in '$filename': $line - $err";
1288 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1289 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1291 eval { pmg_verify_transport_domain_or_email
($domain); };
1293 $parse_error->($err);
1297 if ($host =~ m/^\[(.*)\]$/) {
1301 $use_mx = 0 if ($protocol eq "lmtp");
1303 eval { pmg_verify_transport_address
($host); };
1305 $parse_error->($err);
1311 protocol
=> $protocol,
1315 comment
=> $comment,
1317 $res->{$domain} = $data;
1320 $parse_error->('wrong format');
1327 sub write_transport_map
{
1328 my ($filename, $fh, $tmap) = @_;
1332 foreach my $domain (sort keys %$tmap) {
1333 my $data = $tmap->{$domain};
1335 my $comment = $data->{comment
};
1336 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1337 if defined($comment) && $comment !~ m/^\s*$/;
1339 my $bracket_host = !$data->{use_mx
};
1341 if ($data->{protocol
} eq 'lmtp') {
1343 $data->{protocol
} .= ":inet";
1345 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1346 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1348 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1352 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1353 \
&read_transport_map
,
1354 \
&write_transport_map
,
1355 undef, always_call_parser
=> 1);
1357 # config file generation using templates
1359 sub get_host_dns_info
{
1363 my $nodename = PVE
::INotify
::nodename
();
1365 $dnsinfo->{hostname
} = $nodename;
1366 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1368 my $domain = $resolv->{search
} // 'localdomain';
1369 # postfix will not parse a hostname with trailing '.'
1370 $domain =~ s/^(.*)\.$/$1/;
1371 $dnsinfo->{domain
} = $domain;
1373 $dnsinfo->{fqdn
} = "$nodename.$domain";
1378 sub get_template_vars
{
1381 my $vars = { pmg
=> $self->get_config() };
1383 my $dnsinfo = get_host_dns_info
();
1384 $vars->{dns
} = $dnsinfo;
1385 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1386 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1388 my $transportnets = {};
1394 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1395 foreach my $domain (keys %$tmap) {
1396 my $data = $tmap->{$domain};
1397 my $host = $data->{host
};
1398 if ($host =~ m/^$IPV4RE$/) {
1399 $transportnets->{"$host/32"} = 1;
1400 $mynetworks->{"$host/32"} = 1;
1401 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1402 $transportnets->{"[$1]/128"} = 1;
1403 $mynetworks->{"[$1]/128"} = 1;
1408 $vars->{postfix
}->{transportnets
} = join(' ', sort keys %$transportnets);
1410 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1411 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1412 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1413 $mynetworks->{"[$1]/$2"} = 1;
1415 $mynetworks->{$int_net_cidr} = 1;
1418 if ($int_ip =~ m/^$IPV6RE$/) {
1419 $mynetworks->{"[$int_ip]/128"} = 1;
1421 $mynetworks->{"$int_ip/32"} = 1;
1426 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1427 foreach my $cidr (keys %$netlist) {
1428 my $ip = PVE
::Network
::IP_from_cidr
($cidr);
1431 warn "failed to parse mynetworks entry '$cidr', ignoring\n";
1432 } elsif ($ip->version() == 4) {
1433 $mynetworks->{$ip->prefix()} = 1;
1435 my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
1436 $mynetworks->{$address} = 1;
1440 # add default relay to mynetworks
1441 if (my $relay = $self->get('mail', 'relay')) {
1442 if ($relay =~ m/^$IPV4RE$/) {
1443 $mynetworks->{"$relay/32"} = 1;
1444 } elsif ($relay =~ m/^$IPV6RE$/) {
1445 $mynetworks->{"[$relay]/128"} = 1;
1447 # DNS name - do nothing ?
1451 $vars->{postfix
}->{mynetworks
} = join(' ', sort keys %$mynetworks);
1453 # normalize dnsbl_sites
1454 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1455 if (scalar(@dnsbl_sites)) {
1456 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1459 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1462 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1463 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1464 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1466 if (!defined($int_ip)) {
1467 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1468 $vars->{postfix
}->{int_ip
} = '127.0.0.1';
1469 } elsif ($int_ip =~ m/^$IPV6RE$/) {
1470 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1472 $vars->{postfix
}->{int_ip
} = $int_ip;
1475 my $wlbr = $dnsinfo->{fqdn
};
1476 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1479 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1481 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1483 my $uri = URI-
>new($proxy);
1484 my $host = $uri->host;
1485 my $port = $uri->port // 8080;
1487 my $data = { host
=> $host, port
=> $port };
1488 if (my $ui = $uri->userinfo) {
1489 my ($username, $pw) = split(/:/, $ui, 2);
1490 $data->{username
} = $username;
1491 $data->{password
} = $pw if defined($pw);
1493 $vars->{proxy
} = $data;
1496 warn "parse http_proxy failed - $@" if $@;
1498 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1503 # reads the $filename and checks if it's equal as the $cmp string passed
1504 my sub file_content_equals_str
{
1505 my ($filename, $cmp) = @_;
1507 return if !-f
$filename;
1508 my $current = PVE
::Tools
::file_get_contents
($filename, 128*1024);
1509 return defined($current) && $current eq $cmp; # no change
1512 # use one global TT cache
1513 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1515 my $template_toolkit;
1517 sub get_template_toolkit
{
1519 return $template_toolkit if $template_toolkit;
1521 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1523 return $template_toolkit;
1526 # rewrite file from template
1527 # return true if file has changed
1528 sub rewrite_config_file
{
1529 my ($self, $tmplname, $dstfn) = @_;
1531 my $demo = $self->get('admin', 'demo');
1534 my $demosrc = "$tmplname.demo";
1535 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1538 my ($perm, $uid, $gid);
1540 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1541 # needed if file contains a HTTPProxyPasswort
1543 $uid = getpwnam('clamav');
1544 $gid = getgrnam('adm');
1548 my $tt = get_template_toolkit
();
1550 my $vars = $self->get_template_vars();
1554 $tt->process($tmplname, $vars, \
$output) || die $tt->error() . "\n";
1556 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1558 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1560 if (defined($uid) && defined($gid)) {
1561 chown($uid, $gid, $dstfn);
1567 # rewrite spam configuration
1568 sub rewrite_config_spam
{
1571 my $use_awl = $self->get('spam', 'use_awl');
1572 my $use_bayes = $self->get('spam', 'use_bayes');
1573 my $use_razor = $self->get('spam', 'use_razor');
1577 # delete AW and bayes databases if those features are disabled
1579 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1583 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1584 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1585 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1588 # make sure we have the custom SA files (else cluster sync fails)
1589 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1590 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1592 $changes = 1 if $self->rewrite_config_file(
1593 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1595 $changes = 1 if $self->rewrite_config_file(
1596 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1598 $changes = 1 if $self->rewrite_config_file(
1599 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1601 $changes = 1 if $self->rewrite_config_file(
1602 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1604 $changes = 1 if $self->rewrite_config_file(
1605 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
1607 $changes = 1 if $self->rewrite_config_file(
1608 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
1611 mkdir "/root/.razor";
1613 $changes = 1 if $self->rewrite_config_file(
1614 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1616 if (! -e
'/root/.razor/identity') {
1619 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1620 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1623 syslog
('info', "registering razor failed: $err") if $err;
1630 # rewrite ClamAV configuration
1631 sub rewrite_config_clam
{
1634 return $self->rewrite_config_file(
1635 'clamd.conf.in', '/etc/clamav/clamd.conf');
1638 sub rewrite_config_freshclam
{
1641 return $self->rewrite_config_file(
1642 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1645 sub rewrite_config_postgres
{
1648 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1649 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1653 $changes = 1 if $self->rewrite_config_file(
1654 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1656 $changes = 1 if $self->rewrite_config_file(
1657 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1662 # rewrite /root/.forward
1663 sub rewrite_dot_forward
{
1666 my $dstfn = '/root/.forward';
1668 my $email = $self->get('admin', 'email');
1671 if ($email && $email =~ m/\s*(\S+)\s*/) {
1674 # empty .forward does not forward mails (see man local)
1676 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1678 PVE
::Tools
::file_set_contents
($dstfn, $output);
1683 my $write_smtp_whitelist = sub {
1684 my ($filename, $data, $action) = @_;
1686 $action = 'OK' if !$action;
1689 foreach my $k (sort keys %$data) {
1690 $new .= "$k $action\n";
1692 return 0 if file_content_equals_str
($filename, $new); # no change -> nothing to do
1694 PVE
::Tools
::file_set_contents
($filename, $new);
1696 PMG
::Utils
::run_postmap
($filename);
1701 sub rewrite_postfix_whitelist
{
1702 my ($rulecache) = @_;
1704 # see man page for regexp_table for postfix regex table format
1706 # we use a hash to avoid duplicate entries in regex tables
1709 my $clientlist = {};
1711 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1712 my $oclass = ref($obj);
1713 if ($oclass eq 'PMG::RuleDB::Receiver') {
1714 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1715 $tolist->{"/^$addr\$/"} = 1;
1716 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1717 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1718 $tolist->{"/^.+\@$addr\$/"} = 1;
1719 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1720 my $addr = $obj->{address
};
1722 $tolist->{"/^$addr\$/"} = 1;
1726 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1727 my $oclass = ref($obj);
1728 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1729 if ($oclass eq 'PMG::RuleDB::EMail') {
1730 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1731 $fromlist->{"/^$addr\$/"} = 1;
1732 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1733 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1734 $fromlist->{"/^.+\@$addr\$/"} = 1;
1735 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1736 my $addr = $obj->{address
};
1738 $fromlist->{"/^$addr\$/"} = 1;
1739 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1740 $clientlist->{$obj->{address
}} = 1;
1741 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1742 $clientlist->{$obj->{address
}} = 1;
1746 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1747 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1748 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1749 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1752 # rewrite /etc/postfix/*
1753 sub rewrite_config_postfix
{
1754 my ($self, $rulecache) = @_;
1756 # make sure we have required files (else postfix start fails)
1757 IO
::File-
>new($transport_map_filename, 'a', 0644);
1761 if ($self->get('mail', 'tls')) {
1763 PMG
::Utils
::gen_proxmox_tls_cert
();
1765 syslog
('info', "generating certificate failed: $@") if $@;
1768 $changes = 1 if $self->rewrite_config_file(
1769 'main.cf.in', '/etc/postfix/main.cf');
1771 $changes = 1 if $self->rewrite_config_file(
1772 'master.cf.in', '/etc/postfix/master.cf');
1774 # make sure we have required files (else postfix start fails)
1775 # Note: postmap need a valid /etc/postfix/main.cf configuration
1776 postmap_pmg_domains
();
1777 postmap_pmg_transport
();
1778 postmap_tls_policy
();
1779 postmap_tls_inbound_domains
();
1781 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1783 # make sure aliases.db is up to date
1784 system('/usr/bin/newaliases');
1789 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1790 my $pmg_service_params = {
1799 dkim_sign_all_mail
=> 1,
1803 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1804 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1806 sub dump_smtp_filter_config
{
1811 foreach my $sec (sort keys %$pmg_service_params) {
1812 my $conf_sec = $self->{ids
}->{$sec} // {};
1813 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1814 $val = $conf_sec->{$key};
1815 $conf .= "$sec.$key:$val\n" if defined($val);
1822 sub compare_smtp_filter_config
{
1828 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1832 syslog
('warning', "reloading pmg-smtp-filter: $err");
1835 my $new = $self->dump_smtp_filter_config();
1836 $ret = 1 if $old ne $new;
1839 $self->write_smtp_filter_config() if $ret;
1844 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1846 sub write_smtp_filter_config
{
1849 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1850 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1851 $self->dump_smtp_filter_config());
1857 sub rewrite_config
{
1858 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1860 $force_restart = {} if ! $force_restart;
1862 my $log_restart = sub {
1863 syslog
('info', "configuration change detected for '$_[0]', restarting");
1866 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1867 $force_restart->{postfix
}) {
1868 $log_restart->('postfix');
1869 PMG
::Utils
::service_cmd
('postfix', 'reload');
1872 if ($self->rewrite_dot_forward() && $restart_services) {
1873 # no need to restart anything
1876 if ($self->rewrite_config_postgres() && $restart_services) {
1877 # do nothing (too many side effects)?
1878 # does not happen anyways, because config does not change.
1881 if (($self->rewrite_config_spam() && $restart_services) ||
1882 $force_restart->{spam
}) {
1883 $log_restart->('pmg-smtp-filter');
1884 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1887 if (($self->rewrite_config_clam() && $restart_services) ||
1888 $force_restart->{clam
}) {
1889 $log_restart->('clamav-daemon');
1890 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1893 if (($self->rewrite_config_freshclam() && $restart_services) ||
1894 $force_restart->{freshclam
}) {
1895 $log_restart->('clamav-freshclam');
1896 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1899 if (($self->compare_smtp_filter_config() && $restart_services) ||
1900 $force_restart->{spam
}) {
1901 syslog
('info', "scheduled reload for pmg-smtp-filter");
1902 PMG
::Utils
::reload_smtp_filter
();