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
137 'dkim-use-domain' => {
138 description
=> "Whether to sign using the address from the header or the envelope.",
140 enum
=> [qw(header envelope)],
141 default => 'envelope',
148 advfilter
=> { optional
=> 1 },
149 avast
=> { optional
=> 1 },
150 clamav
=> { optional
=> 1 },
151 statlifetime
=> { optional
=> 1 },
152 dailyreport
=> { optional
=> 1 },
153 demo
=> { optional
=> 1 },
154 email
=> { optional
=> 1 },
155 http_proxy
=> { optional
=> 1 },
156 custom_check
=> { optional
=> 1 },
157 custom_check_path
=> { optional
=> 1 },
158 dkim_sign
=> { optional
=> 1 },
159 dkim_sign_all_mail
=> { optional
=> 1 },
160 dkim_selector
=> { optional
=> 1 },
161 'dkim-use-domain' => { optional
=> 1 },
165 package PMG
::Config
::Spam
;
170 use base
qw(PMG::Config::Base);
179 description
=> "This option is used to specify which languages are considered OK for incoming mail.",
181 pattern
=> '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
185 description
=> "Whether to use the naive-Bayesian-style classifier.",
190 description
=> "Use the Auto-Whitelist plugin.",
195 description
=> "Whether to use Razor2, if it is available.",
199 wl_bounce_relays
=> {
200 description
=> "Whitelist legitimate bounce relays.",
203 clamav_heuristic_score
=> {
204 description
=> "Score for ClamAV heuristics (Encrypted Archives/Documents, PhishingScanURLs, ...).",
211 description
=> "Additional score for bounce mails.",
218 description
=> "Enable real time blacklists (RBL) checks.",
223 description
=> "Maximum size of spam messages in bytes.",
229 description
=> "Extract text from attachments (doc, pdf, rtf, images) and scan for spam.",
238 use_awl
=> { optional
=> 1 },
239 use_razor
=> { optional
=> 1 },
240 wl_bounce_relays
=> { optional
=> 1 },
241 languages
=> { optional
=> 1 },
242 use_bayes
=> { optional
=> 1 },
243 clamav_heuristic_score
=> { optional
=> 1 },
244 bounce_score
=> { optional
=> 1 },
245 rbl_checks
=> { optional
=> 1 },
246 maxspamsize
=> { optional
=> 1 },
247 extract_text
=> { optional
=> 1 },
251 package PMG
::Config
::SpamQuarantine
;
256 use base
qw(PMG::Config::Base);
265 description
=> "Quarantine life time (days)",
271 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.",
273 enum
=> [qw(ticket ldap ldapticket)],
277 description
=> "Spam report style.",
279 enum
=> [qw(none short verbose custom)],
280 default => 'verbose',
283 description
=> "Allow to view images.",
288 description
=> "Allow to view hyperlinks.",
293 description
=> "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
294 type
=> 'string', format
=> 'address',
297 description
=> "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
304 description
=> "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
306 enum
=> [qw(http https)],
310 description
=> "Text for 'From' header in daily spam report mails.",
314 description
=> "Enables user self-service for Quarantine Links. Caution: this is accessible without authentication",
323 mailfrom
=> { optional
=> 1 },
324 hostname
=> { optional
=> 1 },
325 lifetime
=> { optional
=> 1 },
326 authmode
=> { optional
=> 1 },
327 reportstyle
=> { optional
=> 1 },
328 viewimages
=> { optional
=> 1 },
329 allowhrefs
=> { optional
=> 1 },
330 port
=> { optional
=> 1 },
331 protocol
=> { optional
=> 1 },
332 quarantinelink
=> { optional
=> 1 },
336 package PMG
::Config
::VirusQuarantine
;
341 use base
qw(PMG::Config::Base);
353 lifetime
=> { optional
=> 1 },
354 viewimages
=> { optional
=> 1 },
355 allowhrefs
=> { optional
=> 1 },
359 package PMG
::Config
::ClamAV
;
364 use base
qw(PMG::Config::Base);
373 description
=> "ClamAV database mirror server.",
375 default => 'database.clamav.net',
377 archiveblockencrypted
=> {
378 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'.",
383 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.",
389 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.",
395 description
=> "Files larger than this limit (in bytes) won't be scanned.",
401 description
=> "Sets the maximum amount of data (in bytes) to be scanned for each input file.",
404 default => 100000000,
407 description
=> "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
412 # FIXME: remove for PMG 8.0 - https://blog.clamav.net/2021/04/are-you-still-attempting-to-download.html
414 description
=> "Enables support for Google Safe Browsing. (deprecated option, will be ignored)",
419 description
=> "Enables ScriptedUpdates (incremental download of signatures)",
428 archiveblockencrypted
=> { optional
=> 1 },
429 archivemaxrec
=> { optional
=> 1 },
430 archivemaxfiles
=> { optional
=> 1 },
431 archivemaxsize
=> { optional
=> 1 },
432 maxscansize
=> { optional
=> 1 },
433 dbmirror
=> { optional
=> 1 },
434 maxcccount
=> { optional
=> 1 },
435 safebrowsing
=> { optional
=> 1 }, # FIXME: remove for PMG 8.0
436 scriptedupdates
=> { optional
=> 1},
440 package PMG
::Config
::Mail
;
445 use PVE
::ProcFSTools
;
447 use base
qw(PMG::Config::Base);
454 sub physical_memory
{
456 return $physicalmem if $physicalmem;
458 my $info = PVE
::ProcFSTools
::read_meminfo
();
459 my $total = int($info->{memtotal
} / (1024*1024));
464 # heuristic for optimal number of smtp-filter servers
465 sub get_max_filters
{
467 my $per_server_memory_usage = 150;
469 my $memory = physical_memory
();
471 my $base_memory_usage; # the estimated base load of the system
472 if ($memory < 3840) { # 3.75 GiB
473 my $memory_gb = sprintf('%.1f', $memory/1024.0
);
474 my $warn_str = $memory <= 1900 ?
'minimum 2' : 'recommended 4';
475 warn "system memory size of $memory_gb GiB is below the ${warn_str}+ GiB limit!\n";
477 $base_memory_usage = int($memory * 0.625); # for small system assume 5/8 for base system
478 $base_memory_usage = 512 if $base_memory_usage < 512;
480 $base_memory_usage = 2560; # 2.5 GiB
482 my $add_servers = int(($memory - $base_memory_usage)/$per_server_memory_usage);
483 $max_servers += $add_servers if $add_servers > 0;
484 $max_servers = 40 if $max_servers > 40;
486 return $max_servers - 2;
490 # estimate optimal number of smtpd daemons
492 my $max_servers = 25;
494 my $memory = physical_memory
();
495 my $add_servers = int(($memory - 512)/$servermem);
496 $max_servers += $add_servers if $add_servers > 0;
497 $max_servers = 100 if $max_servers > 100;
502 # estimate optimal number of proxpolicy servers
504 my $memory = physical_memory
();
505 $max_servers = 5 if $memory >= 500;
512 description
=> "SMTP port number for outgoing mail (trusted).",
519 description
=> "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
526 description
=> "The default mail delivery transport (incoming mails).",
527 type
=> 'string', format
=> 'address',
530 description
=> "Transport protocol for relay host.",
532 enum
=> [qw(smtp lmtp)],
536 description
=> "SMTP/LMTP port number for relay host.",
543 description
=> "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
548 description
=> "When set, all outgoing mails are deliverd to the specified smarthost."
549 ." (postfix option `default_transport`)",
550 type
=> 'string', format
=> 'address',
553 description
=> "SMTP port number for smarthost. (postfix option `default_transport`)",
560 description
=> "ESMTP banner.",
563 default => 'ESMTP Proxmox',
566 description
=> "Maximum number of pmg-smtp-filter processes.",
570 default => get_max_filters
(),
573 description
=> "Maximum number of pmgpolicy processes.",
577 default => get_max_policy
(),
580 description
=> "Maximum number of SMTP daemon processes (in).",
584 default => get_max_smtpd
(),
587 description
=> "Maximum number of SMTP daemon processes (out).",
591 default => get_max_smtpd
(),
593 conn_count_limit
=> {
594 description
=> "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
600 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.",
605 message_rate_limit
=> {
606 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.",
612 description
=> "Hide received header in outgoing mails.",
617 description
=> "Maximum email size. Larger mails are rejected. (postfix option `message_size_limit`)",
620 default => 1024*1024*10,
623 description
=> "SMTP delay warning time (in hours). (postfix option `delay_warning_time`)",
629 description
=> "Enable TLS.",
634 description
=> "Enable TLS Logging.",
639 description
=> "Add TLS received header.",
644 description
=> "Use Sender Policy Framework.",
649 description
=> "Use Greylisting for IPv4.",
654 description
=> "Netmask to apply for greylisting IPv4 hosts",
661 description
=> "Use Greylisting for IPv6.",
666 description
=> "Netmask to apply for greylisting IPv6 hosts",
673 description
=> "Use SMTP HELO tests. (postfix option `smtpd_helo_restrictions`)",
678 description
=> "Reject unknown clients. (postfix option `reject_unknown_client_hostname`)",
682 rejectunknownsender
=> {
683 description
=> "Reject unknown senders. (postfix option `reject_unknown_sender_domain`)",
688 description
=> "Enable receiver verification. The value specifies the numerical reply"
689 ." code when the Postfix SMTP server rejects a recipient address."
690 ." (postfix options `reject_unknown_recipient_domain`, `reject_unverified_recipient`,"
691 ." and `unverified_recipient_reject_code`)",
693 enum
=> ['450', '550'],
696 description
=> "Optional list of DNS white/blacklist domains (postfix option `postscreen_dnsbl_sites`).",
697 type
=> 'string', format
=> 'dnsbl-entry-list',
700 description
=> "The inclusive lower bound for blocking a remote SMTP client, based on"
701 ." its combined DNSBL score (postfix option `postscreen_dnsbl_threshold`).",
706 before_queue_filtering
=> {
707 description
=> "Enable before queue filtering by pmg-smtp-filter",
712 description
=> "Send out NDR when mail gets blocked",
717 description
=> "Enable SMTPUTF8 support in Postfix and detection for locally generated mail (postfix option `smtputf8_enable`)",
721 'filter-timeout' => {
722 description
=> "Timeout for the processing of one mail (in seconds) (postfix option"
723 ." `smtpd_proxy_timeout` and `lmtp_data_done_timeout`)",
734 int_port
=> { optional
=> 1 },
735 ext_port
=> { optional
=> 1 },
736 smarthost
=> { optional
=> 1 },
737 smarthostport
=> { optional
=> 1 },
738 relay
=> { optional
=> 1 },
739 relayprotocol
=> { optional
=> 1 },
740 relayport
=> { optional
=> 1 },
741 relaynomx
=> { optional
=> 1 },
742 dwarning
=> { optional
=> 1 },
743 max_smtpd_in
=> { optional
=> 1 },
744 max_smtpd_out
=> { optional
=> 1 },
745 greylist
=> { optional
=> 1 },
746 greylistmask4
=> { optional
=> 1 },
747 greylist6
=> { optional
=> 1 },
748 greylistmask6
=> { optional
=> 1 },
749 helotests
=> { optional
=> 1 },
750 tls
=> { optional
=> 1 },
751 tlslog
=> { optional
=> 1 },
752 tlsheader
=> { optional
=> 1 },
753 spf
=> { optional
=> 1 },
754 maxsize
=> { optional
=> 1 },
755 banner
=> { optional
=> 1 },
756 max_filters
=> { optional
=> 1 },
757 max_policy
=> { optional
=> 1 },
758 hide_received
=> { optional
=> 1 },
759 rejectunknown
=> { optional
=> 1 },
760 rejectunknownsender
=> { optional
=> 1 },
761 conn_count_limit
=> { optional
=> 1 },
762 conn_rate_limit
=> { optional
=> 1 },
763 message_rate_limit
=> { optional
=> 1 },
764 verifyreceivers
=> { optional
=> 1 },
765 dnsbl_sites
=> { optional
=> 1 },
766 dnsbl_threshold
=> { optional
=> 1 },
767 before_queue_filtering
=> { optional
=> 1 },
768 ndr_on_block
=> { optional
=> 1 },
769 smtputf8
=> { optional
=> 1 },
770 'filter-timeout' => { optional
=> 1 },
783 use PVE
::Tools
qw($IPV4RE $IPV6RE);
790 PMG
::Config
::Admin-
>register();
791 PMG
::Config
::Mail-
>register();
792 PMG
::Config
::SpamQuarantine-
>register();
793 PMG
::Config
::VirusQuarantine-
>register();
794 PMG
::Config
::Spam-
>register();
795 PMG
::Config
::ClamAV-
>register();
797 # initialize all plugins
798 PMG
::Config
::Base-
>init();
800 PVE
::JSONSchema
::register_format
(
801 'transport-domain', \
&pmg_verify_transport_domain
);
803 sub pmg_verify_transport_domain
{
804 my ($name, $noerr) = @_;
806 # like dns-name, but can contain leading dot
807 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
809 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
810 return undef if $noerr;
811 die "value does not look like a valid transport domain\n";
816 PVE
::JSONSchema
::register_format
(
817 'transport-domain-or-email', \
&pmg_verify_transport_domain_or_email
);
819 sub pmg_verify_transport_domain_or_email
{
820 my ($name, $noerr) = @_;
822 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
825 if ($name =~ m/^(?:[^\s\/\
@]+\
@)(${namere
}\
.)*${namere
}$/) {
829 # like dns-name, but can contain leading dot
830 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
831 return undef if $noerr;
832 die "value does not look like a valid transport domain or email address\n";
837 PVE
::JSONSchema
::register_format
(
838 'dnsbl-entry', \
&pmg_verify_dnsbl_entry
);
840 sub pmg_verify_dnsbl_entry
{
841 my ($name, $noerr) = @_;
843 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
844 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
845 # we don't implement the ';' separated numbers in pattern, because this
846 # breaks at PVE::JSONSchema::split_list
847 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
849 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
850 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
851 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
852 return undef if $noerr;
853 die "value '$name' does not look like a valid dnsbl entry\n";
861 my $class = ref($type) || $type;
863 my $cfg = PVE
::INotify
::read_file
("pmg.conf");
865 return bless $cfg, $class;
871 PVE
::INotify
::write_file
("pmg.conf", $self);
874 my $lockfile = "/var/lock/pmgconfig.lck";
877 my ($code, $errmsg) = @_;
879 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
881 $errmsg ?
die "$errmsg: $err" : die $err;
887 my ($self, $section, $key, $value) = @_;
889 my $pdata = PMG
::Config
::Base-
>private();
891 my $plugin = $pdata->{plugins
}->{$section};
892 die "no such section '$section'" if !$plugin;
894 if (defined($value)) {
895 my $tmp = PMG
::Config
::Base-
>check_value($section, $key, $value, $section, 0);
896 $self->{ids
}->{$section} = { type
=> $section } if !defined($self->{ids
}->{$section});
897 $self->{ids
}->{$section}->{$key} = PMG
::Config
::Base-
>decode_value($section, $key, $tmp);
899 if (defined($self->{ids
}->{$section})) {
900 delete $self->{ids
}->{$section}->{$key};
907 # get section value or default
909 my ($self, $section, $key, $nodefault) = @_;
911 my $pdata = PMG
::Config
::Base-
>private();
912 my $schema = $pdata->{propertyList
}->{$key} // die "no schema for property '$section/$key'\n";
913 my $options = $pdata->{options
}->{$section} // die "no options for section '$section/$key'\n";
915 die "no such property '$section/$key'\n"
916 if !(defined($schema) && defined($options) && defined($options->{$key}));
918 my $values = $self->{ids
}->{$section};
919 return $values->{$key} if defined($values) && defined($values->{$key});
921 return undef if $nodefault;
923 return $schema->{default};
926 # get a whole section with default value
928 my ($self, $section) = @_;
930 my $pdata = PMG
::Config
::Base-
>private();
931 return undef if !defined($pdata->{options
}->{$section});
935 foreach my $key (keys %{$pdata->{options
}->{$section}}) {
937 my $pdesc = $pdata->{propertyList
}->{$key};
939 if (defined($self->{ids
}->{$section}) &&
940 defined(my $value = $self->{ids
}->{$section}->{$key})) {
941 $res->{$key} = $value;
944 $res->{$key} = $pdesc->{default};
950 # get a whole config with default values
954 my $pdata = PMG
::Config
::Base-
>private();
958 foreach my $type (keys %{$pdata->{plugins
}}) {
959 my $plugin = $pdata->{plugins
}->{$type};
960 $res->{$type} = $self->get_section($type);
967 my ($filename, $fh) = @_;
970 $raw = do { local $/ = undef; <$fh> } if defined($fh);
972 return PMG
::Config
::Base-
>parse_config($filename, $raw);
976 my ($filename, $fh, $cfg) = @_;
978 my $raw = PMG
::Config
::Base-
>write_config($filename, $cfg);
980 PVE
::Tools
::safe_print
($filename, $fh, $raw);
983 PVE
::INotify
::register_file
('pmg.conf', "/etc/pmg/pmg.conf",
986 undef, always_call_parser
=> 1);
988 # parsers/writers for other files
990 my $domainsfilename = "/etc/pmg/domains";
992 sub postmap_pmg_domains
{
993 PMG
::Utils
::run_postmap
($domainsfilename);
996 sub read_pmg_domains
{
997 my ($filename, $fh) = @_;
1003 while (defined(my $line = <$fh>)) {
1005 next if $line =~ m/^\s*$/;
1006 if ($line =~ m/^#(.*)\s*$/) {
1010 if ($line =~ m/^(\S+)\s.*$/) {
1012 $domains->{$domain} = {
1013 domain
=> $domain, comment
=> $comment };
1016 warn "parse error in '$filename': $line\n";
1025 sub write_pmg_domains
{
1026 my ($filename, $fh, $domains) = @_;
1028 foreach my $domain (sort keys %$domains) {
1029 my $comment = $domains->{$domain}->{comment
};
1030 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1031 if defined($comment) && $comment !~ m/^\s*$/;
1033 PVE
::Tools
::safe_print
($filename, $fh, "$domain 1\n");
1037 PVE
::INotify
::register_file
('domains', $domainsfilename,
1039 \
&write_pmg_domains
,
1040 undef, always_call_parser
=> 1);
1042 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
1044 PVE
::INotify
::register_file
('dkimdomains', $dkimdomainsfile,
1046 \
&write_pmg_domains
,
1047 undef, always_call_parser
=> 1);
1049 my $mynetworks_filename = "/etc/pmg/mynetworks";
1051 sub read_pmg_mynetworks
{
1052 my ($filename, $fh) = @_;
1054 my $mynetworks = {};
1058 while (defined(my $line = <$fh>)) {
1060 next if $line =~ m/^\s*$/;
1061 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1062 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1063 my $cidr = "$network/${prefix_size}";
1064 # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
1065 $mynetworks->{$cidr} = {
1067 network_address
=> $network,
1068 prefix_size
=> $prefix_size,
1069 comment
=> $comment // '',
1072 warn "parse error in '$filename': $line\n";
1080 sub write_pmg_mynetworks
{
1081 my ($filename, $fh, $mynetworks) = @_;
1083 foreach my $cidr (sort keys %$mynetworks) {
1084 my $data = $mynetworks->{$cidr};
1085 my $comment = $data->{comment
} // '*';
1086 PVE
::Tools
::safe_print
($filename, $fh, "$cidr #$comment\n");
1090 PVE
::INotify
::register_file
('mynetworks', $mynetworks_filename,
1091 \
&read_pmg_mynetworks
,
1092 \
&write_pmg_mynetworks
,
1093 undef, always_call_parser
=> 1);
1095 PVE
::JSONSchema
::register_format
(
1096 'tls-policy', \
&pmg_verify_tls_policy
);
1098 # TODO: extend to parse attributes of the policy
1099 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1100 sub pmg_verify_tls_policy
{
1101 my ($policy, $noerr) = @_;
1103 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1104 return undef if $noerr;
1105 die "value '$policy' does not look like a valid tls policy\n";
1110 PVE
::JSONSchema
::register_format
(
1111 'tls-policy-strict', \
&pmg_verify_tls_policy_strict
);
1113 sub pmg_verify_tls_policy_strict
{
1114 my ($policy, $noerr) = @_;
1116 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1117 return undef if $noerr;
1118 die "value '$policy' does not look like a valid tls policy\n";
1123 PVE
::JSONSchema
::register_format
(
1124 'transport-domain-or-nexthop', \
&pmg_verify_transport_domain_or_nexthop
);
1126 sub pmg_verify_transport_domain_or_nexthop
{
1127 my ($name, $noerr) = @_;
1129 if (pmg_verify_transport_domain
($name, 1)) {
1131 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1133 if ($nexthop =~ m/^\[(.*)\]$/) {
1136 return $name if pmg_verify_transport_address
($nexthop, 1);
1138 return undef if $noerr;
1139 die "value does not look like a valid domain or next-hop\n";
1143 sub read_tls_policy
{
1144 my ($filename, $fh) = @_;
1146 return {} if !defined($fh);
1148 my $tls_policy = {};
1150 while (defined(my $line = <$fh>)) {
1152 next if $line =~ m/^\s*$/;
1153 next if $line =~ m/^#(.*)\s*$/;
1155 my $parse_error = sub {
1157 warn "parse error in '$filename': $line - $err\n";
1160 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1161 my ($destination, $policy) = ($1, $2);
1164 pmg_verify_transport_domain_or_nexthop
($destination);
1165 pmg_verify_tls_policy
($policy);
1168 $parse_error->($err);
1172 $tls_policy->{$destination} = {
1173 destination
=> $destination,
1177 $parse_error->('wrong format');
1184 sub write_tls_policy
{
1185 my ($filename, $fh, $tls_policy) = @_;
1187 return if !$tls_policy;
1189 foreach my $destination (sort keys %$tls_policy) {
1190 my $entry = $tls_policy->{$destination};
1191 PVE
::Tools
::safe_print
(
1192 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1196 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1197 PVE
::INotify
::register_file
('tls_policy', $tls_policy_map_filename,
1200 undef, always_call_parser
=> 1);
1202 sub postmap_tls_policy
{
1203 PMG
::Utils
::run_postmap
($tls_policy_map_filename);
1206 sub read_tls_inbound_domains
{
1207 my ($filename, $fh) = @_;
1209 return {} if !defined($fh);
1213 while (defined(my $line = <$fh>)) {
1215 next if $line =~ m/^\s*$/;
1216 next if $line =~ m/^#(.*)\s*$/;
1218 my $parse_error = sub {
1220 warn "parse error in '$filename': $line - $err\n";
1223 if ($line =~ m/^(\S+) reject_plaintext_session$/) {
1226 eval { pmg_verify_transport_domain
($domain) };
1228 $parse_error->($err);
1232 $domains->{$domain} = 1;
1234 $parse_error->('wrong format');
1241 sub write_tls_inbound_domains
{
1242 my ($filename, $fh, $domains) = @_;
1244 return if !$domains;
1246 foreach my $domain (sort keys %$domains) {
1247 PVE
::Tools
::safe_print
($filename, $fh, "$domain reject_plaintext_session\n");
1251 my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
1252 PVE
::INotify
::register_file
('tls_inbound_domains', $tls_inbound_domains_map_filename,
1253 \
&read_tls_inbound_domains
,
1254 \
&write_tls_inbound_domains
,
1255 undef, always_call_parser
=> 1);
1257 sub postmap_tls_inbound_domains
{
1258 PMG
::Utils
::run_postmap
($tls_inbound_domains_map_filename);
1261 my $transport_map_filename = "/etc/pmg/transport";
1263 sub postmap_pmg_transport
{
1264 PMG
::Utils
::run_postmap
($transport_map_filename);
1267 PVE
::JSONSchema
::register_format
(
1268 'transport-address', \
&pmg_verify_transport_address
);
1270 sub pmg_verify_transport_address
{
1271 my ($name, $noerr) = @_;
1273 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1275 } elsif (PVE
::JSONSchema
::pve_verify_address
($name, 1)) {
1278 return undef if $noerr;
1279 die "value does not look like a valid address\n";
1283 sub read_transport_map
{
1284 my ($filename, $fh) = @_;
1286 return [] if !defined($fh);
1292 while (defined(my $line = <$fh>)) {
1294 next if $line =~ m/^\s*$/;
1295 if ($line =~ m/^#(.*)\s*$/) {
1300 my $parse_error = sub {
1302 warn "parse error in '$filename': $line - $err";
1306 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1307 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1309 eval { pmg_verify_transport_domain_or_email
($domain); };
1311 $parse_error->($err);
1315 if ($host =~ m/^\[(.*)\]$/) {
1319 $use_mx = 0 if ($protocol eq "lmtp");
1321 eval { pmg_verify_transport_address
($host); };
1323 $parse_error->($err);
1329 protocol
=> $protocol,
1333 comment
=> $comment,
1335 $res->{$domain} = $data;
1338 $parse_error->('wrong format');
1345 sub write_transport_map
{
1346 my ($filename, $fh, $tmap) = @_;
1350 foreach my $domain (sort keys %$tmap) {
1351 my $data = $tmap->{$domain};
1353 my $comment = $data->{comment
};
1354 PVE
::Tools
::safe_print
($filename, $fh, "#$comment\n")
1355 if defined($comment) && $comment !~ m/^\s*$/;
1357 my $bracket_host = !$data->{use_mx
};
1359 if ($data->{protocol
} eq 'lmtp') {
1361 $data->{protocol
} .= ":inet";
1363 $bracket_host = 1 if $data->{host
} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1364 my $host = $bracket_host ?
"[$data->{host}]" : $data->{host
};
1366 PVE
::Tools
::safe_print
($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1370 PVE
::INotify
::register_file
('transport', $transport_map_filename,
1371 \
&read_transport_map
,
1372 \
&write_transport_map
,
1373 undef, always_call_parser
=> 1);
1375 # config file generation using templates
1377 sub get_host_dns_info
{
1381 my $nodename = PVE
::INotify
::nodename
();
1383 $dnsinfo->{hostname
} = $nodename;
1384 my $resolv = PVE
::INotify
::read_file
('resolvconf');
1386 my $domain = $resolv->{search
} // 'localdomain';
1387 # postfix will not parse a hostname with trailing '.'
1388 $domain =~ s/^(.*)\.$/$1/;
1389 $dnsinfo->{domain
} = $domain;
1391 $dnsinfo->{fqdn
} = "$nodename.$domain";
1396 sub get_template_vars
{
1399 my $vars = { pmg
=> $self->get_config() };
1401 my $dnsinfo = get_host_dns_info
();
1402 $vars->{dns
} = $dnsinfo;
1403 my $int_ip = PMG
::Cluster
::remote_node_ip
($dnsinfo->{hostname
});
1404 $vars->{ipconfig
}->{int_ip
} = $int_ip;
1406 my $transportnets = {};
1412 if (my $tmap = PVE
::INotify
::read_file
('transport')) {
1413 foreach my $domain (keys %$tmap) {
1414 my $data = $tmap->{$domain};
1415 my $host = $data->{host
};
1416 if ($host =~ m/^$IPV4RE$/) {
1417 $transportnets->{"$host/32"} = 1;
1418 $mynetworks->{"$host/32"} = 1;
1419 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1420 $transportnets->{"[$1]/128"} = 1;
1421 $mynetworks->{"[$1]/128"} = 1;
1426 $vars->{postfix
}->{transportnets
} = join(' ', sort keys %$transportnets);
1428 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1429 if (my $int_net_cidr = PMG
::Utils
::find_local_network_for_ip
($int_ip, 1)) {
1430 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d
+)$/) {
1431 $mynetworks->{"[$1]/$2"} = 1;
1433 $mynetworks->{$int_net_cidr} = 1;
1436 if ($int_ip =~ m/^$IPV6RE$/) {
1437 $mynetworks->{"[$int_ip]/128"} = 1;
1439 $mynetworks->{"$int_ip/32"} = 1;
1444 my $netlist = PVE
::INotify
::read_file
('mynetworks');
1445 foreach my $cidr (keys %$netlist) {
1446 my $ip = PVE
::Network
::IP_from_cidr
($cidr);
1449 warn "failed to parse mynetworks entry '$cidr', ignoring\n";
1450 } elsif ($ip->version() == 4) {
1451 $mynetworks->{$ip->prefix()} = 1;
1453 my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
1454 $mynetworks->{$address} = 1;
1458 # add default relay to mynetworks
1459 if (my $relay = $self->get('mail', 'relay')) {
1460 if ($relay =~ m/^$IPV4RE$/) {
1461 $mynetworks->{"$relay/32"} = 1;
1462 } elsif ($relay =~ m/^$IPV6RE$/) {
1463 $mynetworks->{"[$relay]/128"} = 1;
1465 # DNS name - do nothing ?
1469 $vars->{postfix
}->{mynetworks
} = join(' ', sort keys %$mynetworks);
1471 # normalize dnsbl_sites
1472 my @dnsbl_sites = PVE
::Tools
::split_list
($vars->{pmg
}->{mail
}->{dnsbl_sites
});
1473 if (scalar(@dnsbl_sites)) {
1474 $vars->{postfix
}->{dnsbl_sites
} = join(',', @dnsbl_sites);
1477 $vars->{postfix
}->{dnsbl_threshold
} = $self->get('mail', 'dnsbl_threshold');
1480 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1481 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1482 $vars->{postfix
}->{usepolicy
} = $usepolicy;
1484 if (!defined($int_ip)) {
1485 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1486 $vars->{postfix
}->{int_ip
} = '127.0.0.1';
1487 } elsif ($int_ip =~ m/^$IPV6RE$/) {
1488 $vars->{postfix
}->{int_ip
} = "[$int_ip]";
1490 $vars->{postfix
}->{int_ip
} = $int_ip;
1493 my $wlbr = $dnsinfo->{fqdn
};
1494 foreach my $r (PVE
::Tools
::split_list
($vars->{pmg
}->{spam
}->{wl_bounce_relays
})) {
1497 $vars->{composed
}->{wl_bounce_relays
} = $wlbr;
1499 if (my $proxy = $vars->{pmg
}->{admin
}->{http_proxy
}) {
1501 my $uri = URI-
>new($proxy);
1502 my $host = $uri->host;
1503 my $port = $uri->port // 8080;
1505 my $data = { host
=> $host, port
=> $port };
1506 if (my $ui = $uri->userinfo) {
1507 my ($username, $pw) = split(/:/, $ui, 2);
1508 $data->{username
} = $username;
1509 $data->{password
} = $pw if defined($pw);
1511 $vars->{proxy
} = $data;
1514 warn "parse http_proxy failed - $@" if $@;
1516 $vars->{postgres
}->{version
} = PMG
::Utils
::get_pg_server_version
();
1521 # reads the $filename and checks if it's equal as the $cmp string passed
1522 my sub file_content_equals_str
{
1523 my ($filename, $cmp) = @_;
1525 return if !-f
$filename;
1526 my $current = PVE
::Tools
::file_get_contents
($filename, 128*1024);
1527 return defined($current) && $current eq $cmp; # no change
1530 # use one global TT cache
1531 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1533 my $template_toolkit;
1535 sub get_template_toolkit
{
1537 return $template_toolkit if $template_toolkit;
1539 $template_toolkit = Template-
>new({ INCLUDE_PATH
=> $tt_include_path });
1541 return $template_toolkit;
1544 # rewrite file from template
1545 # return true if file has changed
1546 sub rewrite_config_file
{
1547 my ($self, $tmplname, $dstfn) = @_;
1549 my $demo = $self->get('admin', 'demo');
1552 my $demosrc = "$tmplname.demo";
1553 $tmplname = $demosrc if -f
"/var/lib/pmg/templates/$demosrc";
1556 my ($perm, $uid, $gid);
1558 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1559 # needed if file contains a HTTPProxyPasswort
1561 $uid = getpwnam('clamav');
1562 $gid = getgrnam('adm');
1566 my $tt = get_template_toolkit
();
1568 my $vars = $self->get_template_vars();
1572 $tt->process($tmplname, $vars, \
$output) || die $tt->error() . "\n";
1574 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1576 PVE
::Tools
::file_set_contents
($dstfn, $output, $perm);
1578 if (defined($uid) && defined($gid)) {
1579 chown($uid, $gid, $dstfn);
1585 # rewrite spam configuration
1586 sub rewrite_config_spam
{
1589 my $use_awl = $self->get('spam', 'use_awl');
1590 my $use_bayes = $self->get('spam', 'use_bayes');
1591 my $use_razor = $self->get('spam', 'use_razor');
1595 # delete AW and bayes databases if those features are disabled
1597 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1601 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1602 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1603 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1606 # make sure we have the custom SA files (else cluster sync fails)
1607 IO
::File-
>new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1608 IO
::File-
>new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1610 $changes = 1 if $self->rewrite_config_file(
1611 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1613 $changes = 1 if $self->rewrite_config_file(
1614 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1616 $changes = 1 if $self->rewrite_config_file(
1617 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1619 $changes = 1 if $self->rewrite_config_file(
1620 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1622 $changes = 1 if $self->rewrite_config_file(
1623 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
1625 $changes = 1 if $self->rewrite_config_file(
1626 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
1629 mkdir "/root/.razor";
1631 $changes = 1 if $self->rewrite_config_file(
1632 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1634 if (! -e
'/root/.razor/identity') {
1637 PVE
::Tools
::run_command
(['razor-admin', '-discover'], timeout
=> $timeout);
1638 PVE
::Tools
::run_command
(['razor-admin', '-register'], timeout
=> $timeout);
1641 syslog
('info', "registering razor failed: $err") if $err;
1648 # rewrite ClamAV configuration
1649 sub rewrite_config_clam
{
1652 return $self->rewrite_config_file(
1653 'clamd.conf.in', '/etc/clamav/clamd.conf');
1656 sub rewrite_config_freshclam
{
1659 return $self->rewrite_config_file(
1660 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1663 sub rewrite_config_postgres
{
1666 my $pg_maj_version = PMG
::Utils
::get_pg_server_version
();
1667 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1671 $changes = 1 if $self->rewrite_config_file(
1672 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1674 $changes = 1 if $self->rewrite_config_file(
1675 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1680 # rewrite /root/.forward
1681 sub rewrite_dot_forward
{
1684 my $dstfn = '/root/.forward';
1686 my $email = $self->get('admin', 'email');
1689 if ($email && $email =~ m/\s*(\S+)\s*/) {
1692 # empty .forward does not forward mails (see man local)
1694 return 0 if file_content_equals_str
($dstfn, $output); # no change -> nothing to do
1696 PVE
::Tools
::file_set_contents
($dstfn, $output);
1701 my $write_smtp_whitelist = sub {
1702 my ($filename, $data, $action) = @_;
1704 $action = 'OK' if !$action;
1707 foreach my $k (sort keys %$data) {
1708 $new .= "$k $action\n";
1710 return 0 if file_content_equals_str
($filename, $new); # no change -> nothing to do
1712 PVE
::Tools
::file_set_contents
($filename, $new);
1714 PMG
::Utils
::run_postmap
($filename);
1719 sub rewrite_postfix_whitelist
{
1720 my ($rulecache) = @_;
1722 # see man page for regexp_table for postfix regex table format
1724 # we use a hash to avoid duplicate entries in regex tables
1727 my $clientlist = {};
1729 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1730 my $oclass = ref($obj);
1731 if ($oclass eq 'PMG::RuleDB::Receiver') {
1732 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1733 $tolist->{"/^$addr\$/"} = 1;
1734 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1735 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1736 $tolist->{"/^.+\@$addr\$/"} = 1;
1737 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1738 my $addr = $obj->{address
};
1740 $tolist->{"/^$addr\$/"} = 1;
1744 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1745 my $oclass = ref($obj);
1746 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1747 if ($oclass eq 'PMG::RuleDB::EMail') {
1748 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1749 $fromlist->{"/^$addr\$/"} = 1;
1750 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1751 my $addr = PMG
::Utils
::quote_regex
($obj->{address
});
1752 $fromlist->{"/^.+\@$addr\$/"} = 1;
1753 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1754 my $addr = $obj->{address
};
1756 $fromlist->{"/^$addr\$/"} = 1;
1757 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1758 $clientlist->{$obj->{address
}} = 1;
1759 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1760 $clientlist->{$obj->{address
}} = 1;
1764 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1765 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1766 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1767 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1770 # rewrite /etc/postfix/*
1771 sub rewrite_config_postfix
{
1772 my ($self, $rulecache) = @_;
1774 # make sure we have required files (else postfix start fails)
1775 IO
::File-
>new($transport_map_filename, 'a', 0644);
1779 if ($self->get('mail', 'tls')) {
1781 PMG
::Utils
::gen_proxmox_tls_cert
();
1783 syslog
('info', "generating certificate failed: $@") if $@;
1786 $changes = 1 if $self->rewrite_config_file(
1787 'main.cf.in', '/etc/postfix/main.cf');
1789 $changes = 1 if $self->rewrite_config_file(
1790 'master.cf.in', '/etc/postfix/master.cf');
1792 # make sure we have required files (else postfix start fails)
1793 # Note: postmap need a valid /etc/postfix/main.cf configuration
1794 postmap_pmg_domains
();
1795 postmap_pmg_transport
();
1796 postmap_tls_policy
();
1797 postmap_tls_inbound_domains
();
1799 rewrite_postfix_whitelist
($rulecache) if $rulecache;
1801 # make sure aliases.db is up to date
1802 system('/usr/bin/newaliases');
1807 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1808 my $pmg_service_params = {
1817 dkim_sign_all_mail
=> 1,
1818 'dkim-use-domain' => 1,
1822 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1823 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1825 sub dump_smtp_filter_config
{
1830 foreach my $sec (sort keys %$pmg_service_params) {
1831 my $conf_sec = $self->{ids
}->{$sec} // {};
1832 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1833 $val = $conf_sec->{$key};
1834 $conf .= "$sec.$key:$val\n" if defined($val);
1841 sub compare_smtp_filter_config
{
1847 $old = PVE
::Tools
::file_get_contents
($smtp_filter_cfg);
1851 syslog
('warning', "reloading pmg-smtp-filter: $err");
1854 my $new = $self->dump_smtp_filter_config();
1855 $ret = 1 if $old ne $new;
1858 $self->write_smtp_filter_config() if $ret;
1863 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1865 sub write_smtp_filter_config
{
1868 PVE
::Tools
::lock_file
($smtp_filter_cfg_lock, undef, sub {
1869 PVE
::Tools
::file_set_contents
($smtp_filter_cfg,
1870 $self->dump_smtp_filter_config());
1876 sub rewrite_config
{
1877 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1879 $force_restart = {} if ! $force_restart;
1881 my $log_restart = sub {
1882 syslog
('info', "configuration change detected for '$_[0]', restarting");
1885 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1886 $force_restart->{postfix
}) {
1887 $log_restart->('postfix');
1888 PMG
::Utils
::service_cmd
('postfix', 'reload');
1891 if ($self->rewrite_dot_forward() && $restart_services) {
1892 # no need to restart anything
1895 if ($self->rewrite_config_postgres() && $restart_services) {
1896 # do nothing (too many side effects)?
1897 # does not happen anyways, because config does not change.
1900 if (($self->rewrite_config_spam() && $restart_services) ||
1901 $force_restart->{spam
}) {
1902 $log_restart->('pmg-smtp-filter');
1903 PMG
::Utils
::service_cmd
('pmg-smtp-filter', 'restart');
1906 if (($self->rewrite_config_clam() && $restart_services) ||
1907 $force_restart->{clam
}) {
1908 $log_restart->('clamav-daemon');
1909 PMG
::Utils
::service_cmd
('clamav-daemon', 'restart');
1912 if (($self->rewrite_config_freshclam() && $restart_services) ||
1913 $force_restart->{freshclam
}) {
1914 $log_restart->('clamav-freshclam');
1915 PMG
::Utils
::service_cmd
('clamav-freshclam', 'restart');
1918 if (($self->compare_smtp_filter_config() && $restart_services) ||
1919 $force_restart->{spam
}) {
1920 syslog
('info', "scheduled reload for pmg-smtp-filter");
1921 PMG
::Utils
::reload_smtp_filter
();