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 # heuristic for optimal number of smtp-filter servers
458 sub get_max_filters
{
460 my $per_server_memory_usage = 150;
462 my $memory = physical_memory
();
464 my $base_memory_usage; # the estimated base load of the system
465 if ($memory < 3840) { # 3.75 GiB
466 my $memory_gb = sprintf('%.1f', $memory/1024.0
);
467 my $warn_str = $memory <= 1900 ?
'minimum 2' : 'recommended 4';
468 warn "system memory size of $memory_gb GiB is below the ${warn_str}+ GiB limit!\n";
470 $base_memory_usage = int($memory * 0.625); # for small system assume 5/8 for base system
471 $base_memory_usage = 512 if $base_memory_usage < 512;
473 $base_memory_usage = 2560; # 2.5 GiB
475 my $add_servers = int(($memory - $base_memory_usage)/$per_server_memory_usage);
476 $max_servers += $add_servers if $add_servers > 0;
477 $max_servers = 40 if $max_servers > 40;
479 return $max_servers - 2;
483 # estimate optimal number of smtpd daemons
485 my $max_servers = 25;
487 my $memory = physical_memory
();
488 my $add_servers = int(($memory - 512)/$servermem);
489 $max_servers += $add_servers if $add_servers > 0;
490 $max_servers = 100 if $max_servers > 100;
495 # estimate optimal number of proxpolicy servers
497 my $memory = physical_memory
();
498 $max_servers = 5 if $memory >= 500;
505 description
=> "SMTP port number for outgoing mail (trusted).",
512 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
519 description
=> "The default mail delivery transport (incoming mails).",
520 type
=> 'string', format
=> 'address',
523 description
=> "Transport protocol for relay host.",
525 enum
=> [qw(smtp lmtp)],
529 description
=> "SMTP/LMTP port number for relay host.",
536 description
=> "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
541 description
=> "When set, all outgoing mails are deliverd to the specified smarthost."
542 ." (postfix option `default_transport`)",
543 type
=> 'string', format
=> 'address',
546 description
=> "SMTP port number for smarthost. (postfix option `default_transport`)",
553 description
=> "ESMTP banner.",
556 default => 'ESMTP Proxmox',
559 description
=> "Maximum number of pmg-smtp-filter processes.",
563 default => get_max_filters
(),
566 description
=> "Maximum number of pmgpolicy processes.",
570 default => get_max_policy
(),
573 description
=> "Maximum number of SMTP daemon processes (in).",
577 default => get_max_smtpd
(),
580 description
=> "Maximum number of SMTP daemon processes (out).",
584 default => get_max_smtpd
(),
586 conn_count_limit
=> {
587 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
593 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.",
598 message_rate_limit
=> {
599 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.",
605 description
=> "Hide received header in outgoing mails.",
610 description
=> "Maximum email size. Larger mails are rejected. (postfix option `message_size_limit`)",
613 default => 1024*1024*10,
616 description
=> "SMTP delay warning time (in hours). (postfix option `delay_warning_time`)",
622 description
=> "Enable TLS.",
627 description
=> "Enable TLS Logging.",
632 description
=> "Add TLS received header.",
637 description
=> "Use Sender Policy Framework.",
642 description
=> "Use Greylisting for IPv4.",
647 description
=> "Netmask to apply for greylisting IPv4 hosts",
654 description
=> "Use Greylisting for IPv6.",
659 description
=> "Netmask to apply for greylisting IPv6 hosts",
666 description
=> "Use SMTP HELO tests. (postfix option `smtpd_helo_restrictions`)",
671 description
=> "Reject unknown clients. (postfix option `reject_unknown_client_hostname`)",
675 rejectunknownsender
=> {
676 description
=> "Reject unknown senders. (postfix option `reject_unknown_sender_domain`)",
681 description
=> "Enable receiver verification. The value spefifies the numerical reply"
682 ." code when the Postfix SMTP server rejects a recipient address."
683 ." (postfix options `reject_unknown_recipient_domain`, `reject_unverified_recipient`,"
684 ." and `unverified_recipient_reject_code`)",
686 enum
=> ['450', '550'],
689 description
=> "Optional list of DNS white/blacklist domains (postfix option `postscreen_dnsbl_sites`).",
690 type
=> 'string', format
=> 'dnsbl-entry-list',
693 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on"
694 ." its combined DNSBL score (postfix option `postscreen_dnsbl_threshold`).",
699 before_queue_filtering
=> {
700 description
=> "Enable before queue filtering by pmg-smtp-filter",
705 description
=> "Send out NDR when mail gets blocked",
710 description
=> "Enable SMTPUTF8 support in Postfix and detection for locally generated mail (postfix option `smtputf8_enable`)",
719 int_port
=> { optional
=> 1 },
720 ext_port
=> { optional
=> 1 },
721 smarthost
=> { optional
=> 1 },
722 smarthostport
=> { optional
=> 1 },
723 relay
=> { optional
=> 1 },
724 relayprotocol
=> { optional
=> 1 },
725 relayport
=> { optional
=> 1 },
726 relaynomx
=> { optional
=> 1 },
727 dwarning
=> { optional
=> 1 },
728 max_smtpd_in
=> { optional
=> 1 },
729 max_smtpd_out
=> { optional
=> 1 },
730 greylist
=> { optional
=> 1 },
731 greylistmask4
=> { optional
=> 1 },
732 greylist6
=> { optional
=> 1 },
733 greylistmask6
=> { optional
=> 1 },
734 helotests
=> { optional
=> 1 },
735 tls
=> { optional
=> 1 },
736 tlslog
=> { optional
=> 1 },
737 tlsheader
=> { optional
=> 1 },
738 spf
=> { optional
=> 1 },
739 maxsize
=> { optional
=> 1 },
740 banner
=> { optional
=> 1 },
741 max_filters
=> { optional
=> 1 },
742 max_policy
=> { optional
=> 1 },
743 hide_received
=> { optional
=> 1 },
744 rejectunknown
=> { optional
=> 1 },
745 rejectunknownsender
=> { optional
=> 1 },
746 conn_count_limit
=> { optional
=> 1 },
747 conn_rate_limit
=> { optional
=> 1 },
748 message_rate_limit
=> { optional
=> 1 },
749 verifyreceivers
=> { optional
=> 1 },
750 dnsbl_sites
=> { optional
=> 1 },
751 dnsbl_threshold
=> { optional
=> 1 },
752 before_queue_filtering
=> { optional
=> 1 },
753 ndr_on_block
=> { optional
=> 1 },
754 smtputf8
=> { optional
=> 1 },
767 use PVE
::Tools
qw($IPV4RE $IPV6RE);
774 PMG
::Config
::Admin-
>register();
775 PMG
::Config
::Mail-
>register();
776 PMG
::Config
::SpamQuarantine-
>register();
777 PMG
::Config
::VirusQuarantine-
>register();
778 PMG
::Config
::Spam-
>register();
779 PMG
::Config
::ClamAV-
>register();
781 # initialize all plugins
782 PMG
::Config
::Base-
>init();
784 PVE
::JSONSchema
::register_format
(
785 'transport-domain', \
&pmg_verify_transport_domain
);
787 sub pmg_verify_transport_domain
{
788 my ($name, $noerr) = @_;
790 # like dns-name, but can contain leading dot
791 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
793 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
794 return undef if $noerr;
795 die "value does not look like a valid transport domain\n";
800 PVE
::JSONSchema
::register_format
(
801 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
803 sub pmg_verify_transport_domain_or_email
{
804 my ($name, $noerr) = @_;
806 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
809 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
813 # like dns-name, but can contain leading dot
814 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
815 return undef if $noerr;
816 die "value does not look like a valid transport domain or email address\n";
821 PVE
::JSONSchema
::register_format
(
822 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
824 sub pmg_verify_dnsbl_entry
{
825 my ($name, $noerr) = @_;
827 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
828 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
829 # we don't implement the ';' separated numbers in pattern, because this
830 # breaks at PVE::JSONSchema::split_list
831 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
833 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
834 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
835 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
836 return undef if $noerr;
837 die "value '$name' does not look like a valid dnsbl entry\n";
845 my $class = ref($type) || $type;
847 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
849 return bless $cfg, $class;
855 PVE
::INotify
::write_file
("pmg.conf", $self);
858 my $lockfile = "/var/lock/pmgconfig.lck";
861 my ($code, $errmsg) = @_;
863 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
865 $errmsg ?
die "$errmsg: $err" : die $err;
871 my ($self, $section, $key, $value) = @_;
873 my $pdata = PMG
::Config
::Base-
>private();
875 my $plugin = $pdata->{plugins
}->{$section};
876 die "no such section '$section'" if !$plugin;
878 if (defined($value)) {
879 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
880 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
881 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
883 if (defined($self->{ids
}->{$section})) {
884 delete $self->{ids
}->{$section}->{$key};
891 # get section value or default
893 my ($self, $section, $key, $nodefault) = @_;
895 my $pdata = PMG
::Config
::Base-
>private();
896 my $schema = $pdata->{propertyList
}->{$key} // die "no schema for property '$section/$key'\n";
897 my $options = $pdata->{options
}->{$section} // die "no options for section '$section/$key'\n";
899 die "no such property '$section/$key'\n"
900 if !(defined($schema) && defined($options) && defined($options->{$key}));
902 my $values = $self->{ids
}->{$section};
903 return $values->{$key} if defined($values) && defined($values->{$key});
905 return undef if $nodefault;
907 return $schema->{default};
910 # get a whole section with default value
912 my ($self, $section) = @_;
914 my $pdata = PMG
::Config
::Base-
>private();
915 return undef if !defined($pdata->{options
}->{$section});
919 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
921 my $pdesc = $pdata->{propertyList
}->{$key};
923 if (defined($self->{ids
}->{$section}) &&
924 defined(my $value = $self->{ids
}->{$section}->{$key})) {
925 $res->{$key} = $value;
928 $res->{$key} = $pdesc->{default};
934 # get a whole config with default values
938 my $pdata = PMG
::Config
::Base-
>private();
942 foreach my $type (keys %{$pdata->{plugins
}}) {
943 my $plugin = $pdata->{plugins
}->{$type};
944 $res->{$type} = $self->get_section($type);
951 my ($filename, $fh) = @_;
954 $raw = do { local $/ = undef; <$fh> } if defined($fh);
956 return PMG
::Config
::Base-
>parse_config($filename, $raw);
960 my ($filename, $fh, $cfg) = @_;
962 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
964 PVE
::Tools
::safe_print
($filename, $fh, $raw);
967 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
970 undef, always_call_parser
=> 1);
972 # parsers/writers for other files
974 my $domainsfilename = "/etc/pmg/domains";
976 sub postmap_pmg_domains
{
977 PMG
::Utils
::run_postmap
($domainsfilename);
980 sub read_pmg_domains
{
981 my ($filename, $fh) = @_;
987 while (defined(my $line = <$fh>)) {
989 next if $line =~ m/^\s*$/;
990 if ($line =~ m/^#(.*)\s*$/) {
994 if ($line =~ m/^(\S+)\s.*$/) {
996 $domains->{$domain} = {
997 domain
=> $domain, comment
=> $comment };
1000 warn "parse error in '$filename': $line\n";
1009 sub write_pmg_domains
{
1010 my ($filename, $fh, $domains) = @_;
1012 foreach my $domain (sort keys %$domains) {
1013 my $comment = $domains->{$domain}->{comment
};
1014 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1015 if defined($comment) && $comment !~ m/^\s*$/;
1017 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
1021 PVE
::INotify
::register_file
('domains', $domainsfilename,
1023 \
&write_pmg_domains
,
1024 undef, always_call_parser
=> 1);
1026 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
1028 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
1030 \
&write_pmg_domains
,
1031 undef, always_call_parser
=> 1);
1033 my $mynetworks_filename = "/etc/pmg/mynetworks";
1035 sub read_pmg_mynetworks
{
1036 my ($filename, $fh) = @_;
1038 my $mynetworks = {};
1042 while (defined(my $line = <$fh>)) {
1044 next if $line =~ m/^\s*$/;
1045 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1046 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1047 my $cidr = "$network/${prefix_size}";
1048 # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
1049 $mynetworks->{$cidr} = {
1051 network_address
=> $network,
1052 prefix_size
=> $prefix_size,
1053 comment
=> $comment // '',
1056 warn "parse error in '$filename': $line\n";
1064 sub write_pmg_mynetworks
{
1065 my ($filename, $fh, $mynetworks) = @_;
1067 foreach my $cidr (sort keys %$mynetworks) {
1068 my $data = $mynetworks->{$cidr};
1069 my $comment = $data->{comment
} // '*';
1070 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1074 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1075 \
&read_pmg_mynetworks
,
1076 \
&write_pmg_mynetworks
,
1077 undef, always_call_parser
=> 1);
1079 PVE
::JSONSchema
::register_format
(
1080 'tls-policy', \
&pmg_verify_tls_policy
);
1082 # TODO: extend to parse attributes of the policy
1083 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1084 sub pmg_verify_tls_policy
{
1085 my ($policy, $noerr) = @_;
1087 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1088 return undef if $noerr;
1089 die "value '$policy' does not look like a valid tls policy\n";
1094 PVE
::JSONSchema
::register_format
(
1095 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1097 sub pmg_verify_tls_policy_strict
{
1098 my ($policy, $noerr) = @_;
1100 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1101 return undef if $noerr;
1102 die "value '$policy' does not look like a valid tls policy\n";
1107 PVE
::JSONSchema
::register_format
(
1108 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1110 sub pmg_verify_transport_domain_or_nexthop
{
1111 my ($name, $noerr) = @_;
1113 if (pmg_verify_transport_domain
($name, 1)) {
1115 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1117 if ($nexthop =~ m/^\[(.*)\]$/) {
1120 return $name if pmg_verify_transport_address
($nexthop, 1);
1122 return undef if $noerr;
1123 die "value does not look like a valid domain or next-hop\n";
1127 sub read_tls_policy
{
1128 my ($filename, $fh) = @_;
1130 return {} if !defined($fh);
1132 my $tls_policy = {};
1134 while (defined(my $line = <$fh>)) {
1136 next if $line =~ m/^\s*$/;
1137 next if $line =~ m/^#(.*)\s*$/;
1139 my $parse_error = sub {
1141 warn "parse error in '$filename': $line - $err\n";
1144 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1145 my ($destination, $policy) = ($1, $2);
1148 pmg_verify_transport_domain_or_nexthop
($destination);
1149 pmg_verify_tls_policy
($policy);
1152 $parse_error->($err);
1156 $tls_policy->{$destination} = {
1157 destination
=> $destination,
1161 $parse_error->('wrong format');
1168 sub write_tls_policy
{
1169 my ($filename, $fh, $tls_policy) = @_;
1171 return if !$tls_policy;
1173 foreach my $destination (sort keys %$tls_policy) {
1174 my $entry = $tls_policy->{$destination};
1175 PVE
::Tools
::safe_print
(
1176 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1180 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1181 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1184 undef, always_call_parser
=> 1);
1186 sub postmap_tls_policy
{
1187 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1190 sub read_tls_inbound_domains
{
1191 my ($filename, $fh) = @_;
1193 return {} if !defined($fh);
1197 while (defined(my $line = <$fh>)) {
1199 next if $line =~ m/^\s*$/;
1200 next if $line =~ m/^#(.*)\s*$/;
1202 my $parse_error = sub {
1204 warn "parse error in '$filename': $line - $err\n";
1207 if ($line =~ m/^(\S+) reject_plaintext_session$/) {
1210 eval { pmg_verify_transport_domain
($domain) };
1212 $parse_error->($err);
1216 $domains->{$domain} = 1;
1218 $parse_error->('wrong format');
1225 sub write_tls_inbound_domains
{
1226 my ($filename, $fh, $domains) = @_;
1228 return if !$domains;
1230 foreach my $domain (sort keys %$domains) {
1231 PVE
::Tools
::safe_print
($filename, $fh, "$domain reject_plaintext_session\n");
1235 my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
1236 PVE
::INotify
::register_file
('tls_inbound_domains', $tls_inbound_domains_map_filename,
1237 \
&read_tls_inbound_domains
,
1238 \
&write_tls_inbound_domains
,
1239 undef, always_call_parser
=> 1);
1241 sub postmap_tls_inbound_domains
{
1242 PMG
::Utils
::run_postmap
($tls_inbound_domains_map_filename);
1245 my $transport_map_filename = "/etc/pmg/transport";
1247 sub postmap_pmg_transport
{
1248 PMG
::Utils
::run_postmap
($transport_map_filename);
1251 PVE
::JSONSchema
::register_format
(
1252 'transport-address', \
&pmg_verify_transport_address
);
1254 sub pmg_verify_transport_address
{
1255 my ($name, $noerr) = @_;
1257 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1259 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1262 return undef if $noerr;
1263 die "value does not look like a valid address\n";
1267 sub read_transport_map
{
1268 my ($filename, $fh) = @_;
1270 return [] if !defined($fh);
1276 while (defined(my $line = <$fh>)) {
1278 next if $line =~ m/^\s*$/;
1279 if ($line =~ m/^#(.*)\s*$/) {
1284 my $parse_error = sub {
1286 warn "parse error in '$filename': $line - $err";
1290 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1291 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1293 eval { pmg_verify_transport_domain_or_email
($domain); };
1295 $parse_error->($err);
1299 if ($host =~ m/^\[(.*)\]$/) {
1303 $use_mx = 0 if ($protocol eq "lmtp");
1305 eval { pmg_verify_transport_address
($host); };
1307 $parse_error->($err);
1313 protocol
=> $protocol,
1317 comment
=> $comment,
1319 $res->{$domain} = $data;
1322 $parse_error->('wrong format');
1329 sub write_transport_map
{
1330 my ($filename, $fh, $tmap) = @_;
1334 foreach my $domain (sort keys %$tmap) {
1335 my $data = $tmap->{$domain};
1337 my $comment = $data->{comment
};
1338 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1339 if defined($comment) && $comment !~ m/^\s*$/;
1341 my $bracket_host = !$data->{use_mx
};
1343 if ($data->{protocol
} eq 'lmtp') {
1345 $data->{protocol
} .= ":inet";
1347 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1348 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1350 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1354 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1355 \
&read_transport_map
,
1356 \
&write_transport_map
,
1357 undef, always_call_parser
=> 1);
1359 # config file generation using templates
1361 sub get_host_dns_info
{
1365 my $nodename = PVE
::INotify
::nodename
();
1367 $dnsinfo->{hostname
} = $nodename;
1368 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1370 my $domain = $resolv->{search
} // 'localdomain';
1371 # postfix will not parse a hostname with trailing '.'
1372 $domain =~ s/^(.*)\.$/$1/;
1373 $dnsinfo->{domain
} = $domain;
1375 $dnsinfo->{fqdn
} = "$nodename.$domain";
1380 sub get_template_vars
{
1383 my $vars = { pmg
=> $self->get_config() };
1385 my $dnsinfo = get_host_dns_info
();
1386 $vars->{dns
} = $dnsinfo;
1387 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1388 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1390 my $transportnets = {};
1396 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1397 foreach my $domain (keys %$tmap) {
1398 my $data = $tmap->{$domain};
1399 my $host = $data->{host
};
1400 if ($host =~ m/^$IPV4RE$/) {
1401 $transportnets->{"$host/32"} = 1;
1402 $mynetworks->{"$host/32"} = 1;
1403 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1404 $transportnets->{"[$1]/128"} = 1;
1405 $mynetworks->{"[$1]/128"} = 1;
1410 $vars->{postfix
}->{transportnets
} = join(' ', sort keys %$transportnets);
1412 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1413 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1414 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1415 $mynetworks->{"[$1]/$2"} = 1;
1417 $mynetworks->{$int_net_cidr} = 1;
1420 if ($int_ip =~ m/^$IPV6RE$/) {
1421 $mynetworks->{"[$int_ip]/128"} = 1;
1423 $mynetworks->{"$int_ip/32"} = 1;
1428 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1429 foreach my $cidr (keys %$netlist) {
1430 my $ip = PVE
::Network
::IP_from_cidr
($cidr);
1433 warn "failed to parse mynetworks entry '$cidr', ignoring\n";
1434 } elsif ($ip->version() == 4) {
1435 $mynetworks->{$ip->prefix()} = 1;
1437 my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
1438 $mynetworks->{$address} = 1;
1442 # add default relay to mynetworks
1443 if (my $relay = $self->get('mail', 'relay')) {
1444 if ($relay =~ m/^$IPV4RE$/) {
1445 $mynetworks->{"$relay/32"} = 1;
1446 } elsif ($relay =~ m/^$IPV6RE$/) {
1447 $mynetworks->{"[$relay]/128"} = 1;
1449 # DNS name - do nothing ?
1453 $vars->{postfix
}->{mynetworks
} = join(' ', sort keys %$mynetworks);
1455 # normalize dnsbl_sites
1456 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1457 if (scalar(@dnsbl_sites)) {
1458 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1461 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1464 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1465 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1466 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1468 if (!defined($int_ip)) {
1469 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1470 $vars->{postfix
}->{int_ip
} = '127.0.0.1';
1471 } elsif ($int_ip =~ m/^$IPV6RE$/) {
1472 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1474 $vars->{postfix
}->{int_ip
} = $int_ip;
1477 my $wlbr = $dnsinfo->{fqdn
};
1478 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1481 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1483 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1485 my $uri = URI-
>new($proxy);
1486 my $host = $uri->host;
1487 my $port = $uri->port // 8080;
1489 my $data = { host
=> $host, port
=> $port };
1490 if (my $ui = $uri->userinfo) {
1491 my ($username, $pw) = split(/:/, $ui, 2);
1492 $data->{username
} = $username;
1493 $data->{password
} = $pw if defined($pw);
1495 $vars->{proxy
} = $data;
1498 warn "parse http_proxy failed - $@" if $@;
1500 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1505 # reads the $filename and checks if it's equal as the $cmp string passed
1506 my sub file_content_equals_str
{
1507 my ($filename, $cmp) = @_;
1509 return if !-f
$filename;
1510 my $current = PVE
::Tools
::file_get_contents
($filename, 128*1024);
1511 return defined($current) && $current eq $cmp; # no change
1514 # use one global TT cache
1515 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1517 my $template_toolkit;
1519 sub get_template_toolkit
{
1521 return $template_toolkit if $template_toolkit;
1523 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1525 return $template_toolkit;
1528 # rewrite file from template
1529 # return true if file has changed
1530 sub rewrite_config_file
{
1531 my ($self, $tmplname, $dstfn) = @_;
1533 my $demo = $self->get('admin', 'demo');
1536 my $demosrc = "$tmplname.demo";
1537 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1540 my ($perm, $uid, $gid);
1542 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1543 # needed if file contains a HTTPProxyPasswort
1545 $uid = getpwnam('clamav');
1546 $gid = getgrnam('adm');
1550 my $tt = get_template_toolkit
();
1552 my $vars = $self->get_template_vars();
1556 $tt->process($tmplname, $vars, \
$output) || die $tt->error() . "\n";
1558 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1560 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1562 if (defined($uid) && defined($gid)) {
1563 chown($uid, $gid, $dstfn);
1569 # rewrite spam configuration
1570 sub rewrite_config_spam
{
1573 my $use_awl = $self->get('spam', 'use_awl');
1574 my $use_bayes = $self->get('spam', 'use_bayes');
1575 my $use_razor = $self->get('spam', 'use_razor');
1579 # delete AW and bayes databases if those features are disabled
1581 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1585 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1586 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1587 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1590 # make sure we have the custom SA files (else cluster sync fails)
1591 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1592 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1594 $changes = 1 if $self->rewrite_config_file(
1595 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1597 $changes = 1 if $self->rewrite_config_file(
1598 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1600 $changes = 1 if $self->rewrite_config_file(
1601 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1603 $changes = 1 if $self->rewrite_config_file(
1604 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1606 $changes = 1 if $self->rewrite_config_file(
1607 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
1609 $changes = 1 if $self->rewrite_config_file(
1610 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
1613 mkdir "/root/.razor";
1615 $changes = 1 if $self->rewrite_config_file(
1616 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1618 if (! -e
'/root/.razor/identity') {
1621 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1622 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1625 syslog
('info', "registering razor failed: $err") if $err;
1632 # rewrite ClamAV configuration
1633 sub rewrite_config_clam
{
1636 return $self->rewrite_config_file(
1637 'clamd.conf.in', '/etc/clamav/clamd.conf');
1640 sub rewrite_config_freshclam
{
1643 return $self->rewrite_config_file(
1644 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1647 sub rewrite_config_postgres
{
1650 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1651 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1655 $changes = 1 if $self->rewrite_config_file(
1656 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1658 $changes = 1 if $self->rewrite_config_file(
1659 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1664 # rewrite /root/.forward
1665 sub rewrite_dot_forward
{
1668 my $dstfn = '/root/.forward';
1670 my $email = $self->get('admin', 'email');
1673 if ($email && $email =~ m/\s*(\S+)\s*/) {
1676 # empty .forward does not forward mails (see man local)
1678 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1680 PVE
::Tools
::file_set_contents
($dstfn, $output);
1685 my $write_smtp_whitelist = sub {
1686 my ($filename, $data, $action) = @_;
1688 $action = 'OK' if !$action;
1691 foreach my $k (sort keys %$data) {
1692 $new .= "$k $action\n";
1694 return 0 if file_content_equals_str
($filename, $new); # no change -> nothing to do
1696 PVE
::Tools
::file_set_contents
($filename, $new);
1698 PMG
::Utils
::run_postmap
($filename);
1703 sub rewrite_postfix_whitelist
{
1704 my ($rulecache) = @_;
1706 # see man page for regexp_table for postfix regex table format
1708 # we use a hash to avoid duplicate entries in regex tables
1711 my $clientlist = {};
1713 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1714 my $oclass = ref($obj);
1715 if ($oclass eq 'PMG::RuleDB::Receiver') {
1716 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1717 $tolist->{"/^$addr\$/"} = 1;
1718 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1719 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1720 $tolist->{"/^.+\@$addr\$/"} = 1;
1721 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1722 my $addr = $obj->{address
};
1724 $tolist->{"/^$addr\$/"} = 1;
1728 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1729 my $oclass = ref($obj);
1730 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1731 if ($oclass eq 'PMG::RuleDB::EMail') {
1732 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1733 $fromlist->{"/^$addr\$/"} = 1;
1734 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1735 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1736 $fromlist->{"/^.+\@$addr\$/"} = 1;
1737 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1738 my $addr = $obj->{address
};
1740 $fromlist->{"/^$addr\$/"} = 1;
1741 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1742 $clientlist->{$obj->{address
}} = 1;
1743 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1744 $clientlist->{$obj->{address
}} = 1;
1748 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1749 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1750 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1751 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1754 # rewrite /etc/postfix/*
1755 sub rewrite_config_postfix
{
1756 my ($self, $rulecache) = @_;
1758 # make sure we have required files (else postfix start fails)
1759 IO
::File-
>new($transport_map_filename, 'a', 0644);
1763 if ($self->get('mail', 'tls')) {
1765 PMG
::Utils
::gen_proxmox_tls_cert
();
1767 syslog
('info', "generating certificate failed: $@") if $@;
1770 $changes = 1 if $self->rewrite_config_file(
1771 'main.cf.in', '/etc/postfix/main.cf');
1773 $changes = 1 if $self->rewrite_config_file(
1774 'master.cf.in', '/etc/postfix/master.cf');
1776 # make sure we have required files (else postfix start fails)
1777 # Note: postmap need a valid /etc/postfix/main.cf configuration
1778 postmap_pmg_domains
();
1779 postmap_pmg_transport
();
1780 postmap_tls_policy
();
1781 postmap_tls_inbound_domains
();
1783 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1785 # make sure aliases.db is up to date
1786 system('/usr/bin/newaliases');
1791 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1792 my $pmg_service_params = {
1801 dkim_sign_all_mail
=> 1,
1805 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1806 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1808 sub dump_smtp_filter_config
{
1813 foreach my $sec (sort keys %$pmg_service_params) {
1814 my $conf_sec = $self->{ids
}->{$sec} // {};
1815 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1816 $val = $conf_sec->{$key};
1817 $conf .= "$sec.$key:$val\n" if defined($val);
1824 sub compare_smtp_filter_config
{
1830 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1834 syslog
('warning', "reloading pmg-smtp-filter: $err");
1837 my $new = $self->dump_smtp_filter_config();
1838 $ret = 1 if $old ne $new;
1841 $self->write_smtp_filter_config() if $ret;
1846 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1848 sub write_smtp_filter_config
{
1851 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1852 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1853 $self->dump_smtp_filter_config());
1859 sub rewrite_config
{
1860 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1862 $force_restart = {} if ! $force_restart;
1864 my $log_restart = sub {
1865 syslog
('info', "configuration change detected for '$_[0]', restarting");
1868 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1869 $force_restart->{postfix
}) {
1870 $log_restart->('postfix');
1871 PMG
::Utils
::service_cmd
('postfix', 'reload');
1874 if ($self->rewrite_dot_forward() && $restart_services) {
1875 # no need to restart anything
1878 if ($self->rewrite_config_postgres() && $restart_services) {
1879 # do nothing (too many side effects)?
1880 # does not happen anyways, because config does not change.
1883 if (($self->rewrite_config_spam() && $restart_services) ||
1884 $force_restart->{spam
}) {
1885 $log_restart->('pmg-smtp-filter');
1886 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1889 if (($self->rewrite_config_clam() && $restart_services) ||
1890 $force_restart->{clam
}) {
1891 $log_restart->('clamav-daemon');
1892 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1895 if (($self->rewrite_config_freshclam() && $restart_services) ||
1896 $force_restart->{freshclam
}) {
1897 $log_restart->('clamav-freshclam');
1898 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1901 if (($self->compare_smtp_filter_config() && $restart_services) ||
1902 $force_restart->{spam
}) {
1903 syslog
('info', "scheduled reload for pmg-smtp-filter");
1904 PMG
::Utils
::reload_smtp_filter
();