]> git.proxmox.com Git - pmg-api.git/blob - PMG/Config.pm
add custom_check handling
[pmg-api.git] / PMG / Config.pm
1 package PMG::Config::Base;
2
3 use strict;
4 use warnings;
5 use URI;
6 use Data::Dumper;
7
8 use PVE::Tools;
9 use PVE::JSONSchema qw(get_standard_option);
10 use PVE::SectionConfig;
11
12 use base qw(PVE::SectionConfig);
13
14 my $defaultData = {
15 propertyList => {
16 type => { description => "Section type." },
17 section => {
18 description => "Section ID.",
19 type => 'string', format => 'pve-configid',
20 },
21 },
22 };
23
24 sub private {
25 return $defaultData;
26 }
27
28 sub format_section_header {
29 my ($class, $type, $sectionId) = @_;
30
31 die "internal error ($type ne $sectionId)" if $type ne $sectionId;
32
33 return "section: $type\n";
34 }
35
36
37 sub parse_section_header {
38 my ($class, $line) = @_;
39
40 if ($line =~ m/^section:\s*(\S+)\s*$/) {
41 my $section = $1;
42 my $errmsg = undef; # set if you want to skip whole section
43 eval { PVE::JSONSchema::pve_verify_configid($section); };
44 $errmsg = $@ if $@;
45 my $config = {}; # to return additional attributes
46 return ($section, $section, $errmsg, $config);
47 }
48 return undef;
49 }
50
51 package PMG::Config::Admin;
52
53 use strict;
54 use warnings;
55
56 use base qw(PMG::Config::Base);
57
58 sub type {
59 return 'admin';
60 }
61
62 sub properties {
63 return {
64 advfilter => {
65 description => "Use advanced filters for statistic.",
66 type => 'boolean',
67 default => 1,
68 },
69 dailyreport => {
70 description => "Send daily reports.",
71 type => 'boolean',
72 default => 1,
73 },
74 statlifetime => {
75 description => "User Statistics Lifetime (days)",
76 type => 'integer',
77 default => 7,
78 minimum => 1,
79 },
80 demo => {
81 description => "Demo mode - do not start SMTP filter.",
82 type => 'boolean',
83 default => 0,
84 },
85 email => {
86 description => "Administrator E-Mail address.",
87 type => 'string', format => 'email',
88 default => 'admin@domain.tld',
89 },
90 http_proxy => {
91 description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
92 type => 'string',
93 pattern => "http://.*",
94 },
95 avast => {
96 description => "Use Avast Virus Scanner (/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.",
97 type => 'boolean',
98 default => 0,
99 },
100 clamav => {
101 description => "Use ClamAV Virus Scanner. This is the default virus scanner and is enabled by default.",
102 type => 'boolean',
103 default => 1,
104 },
105 custom_check => {
106 description => "Use Custom Check Script. The script has to take the defined arguments and can return Virus findings or a Spamscore.",
107 type => 'boolean',
108 default => 0,
109 },
110 custom_check_path => {
111 description => "Absolute Path to the Custom Check Script",
112 type => 'string', pattern => '^/([^/\0]+\/)+[^/\0]+$',
113 default => '/usr/local/bin/pmg-custom-check',
114 },
115 };
116 }
117
118 sub options {
119 return {
120 advfilter => { optional => 1 },
121 avast => { optional => 1 },
122 clamav => { optional => 1 },
123 statlifetime => { optional => 1 },
124 dailyreport => { optional => 1 },
125 demo => { optional => 1 },
126 email => { optional => 1 },
127 http_proxy => { optional => 1 },
128 custom_check => { optional => 1 },
129 custom_check_path => { optional => 1 },
130 };
131 }
132
133 package PMG::Config::Spam;
134
135 use strict;
136 use warnings;
137
138 use base qw(PMG::Config::Base);
139
140 sub type {
141 return 'spam';
142 }
143
144 sub properties {
145 return {
146 languages => {
147 description => "This option is used to specify which languages are considered OK for incoming mail.",
148 type => 'string',
149 pattern => '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
150 default => 'all',
151 },
152 use_bayes => {
153 description => "Whether to use the naive-Bayesian-style classifier.",
154 type => 'boolean',
155 default => 1,
156 },
157 use_awl => {
158 description => "Use the Auto-Whitelist plugin.",
159 type => 'boolean',
160 default => 1,
161 },
162 use_razor => {
163 description => "Whether to use Razor2, if it is available.",
164 type => 'boolean',
165 default => 1,
166 },
167 wl_bounce_relays => {
168 description => "Whitelist legitimate bounce relays.",
169 type => 'string',
170 },
171 clamav_heuristic_score => {
172 description => "Score for ClamAV heuristics (Google Safe Browsing database, PhishingScanURLs, ...).",
173 type => 'integer',
174 minimum => 0,
175 maximum => 1000,
176 default => 3,
177 },
178 bounce_score => {
179 description => "Additional score for bounce mails.",
180 type => 'integer',
181 minimum => 0,
182 maximum => 1000,
183 default => 0,
184 },
185 rbl_checks => {
186 description => "Enable real time blacklists (RBL) checks.",
187 type => 'boolean',
188 default => 1,
189 },
190 maxspamsize => {
191 description => "Maximum size of spam messages in bytes.",
192 type => 'integer',
193 minimum => 64,
194 default => 256*1024,
195 },
196 };
197 }
198
199 sub options {
200 return {
201 use_awl => { optional => 1 },
202 use_razor => { optional => 1 },
203 wl_bounce_relays => { optional => 1 },
204 languages => { optional => 1 },
205 use_bayes => { optional => 1 },
206 clamav_heuristic_score => { optional => 1 },
207 bounce_score => { optional => 1 },
208 rbl_checks => { optional => 1 },
209 maxspamsize => { optional => 1 },
210 };
211 }
212
213 package PMG::Config::SpamQuarantine;
214
215 use strict;
216 use warnings;
217
218 use base qw(PMG::Config::Base);
219
220 sub type {
221 return 'spamquar';
222 }
223
224 sub properties {
225 return {
226 lifetime => {
227 description => "Quarantine life time (days)",
228 type => 'integer',
229 minimum => 1,
230 default => 7,
231 },
232 authmode => {
233 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.",
234 type => 'string',
235 enum => [qw(ticket ldap ldapticket)],
236 default => 'ticket',
237 },
238 reportstyle => {
239 description => "Spam report style.",
240 type => 'string',
241 enum => [qw(none short verbose custom)],
242 default => 'verbose',
243 },
244 viewimages => {
245 description => "Allow to view images.",
246 type => 'boolean',
247 default => 1,
248 },
249 allowhrefs => {
250 description => "Allow to view hyperlinks.",
251 type => 'boolean',
252 default => 1,
253 },
254 hostname => {
255 description => "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
256 type => 'string', format => 'address',
257 },
258 port => {
259 description => "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
260 type => 'integer',
261 minimum => 1,
262 maximum => 65535,
263 default => 8006,
264 },
265 protocol => {
266 description => "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
267 type => 'string',
268 enum => [qw(http https)],
269 default => 'https',
270 },
271 mailfrom => {
272 description => "Text for 'From' header in daily spam report mails.",
273 type => 'string',
274 },
275 };
276 }
277
278 sub options {
279 return {
280 mailfrom => { optional => 1 },
281 hostname => { optional => 1 },
282 lifetime => { optional => 1 },
283 authmode => { optional => 1 },
284 reportstyle => { optional => 1 },
285 viewimages => { optional => 1 },
286 allowhrefs => { optional => 1 },
287 port => { optional => 1 },
288 protocol => { optional => 1 },
289 };
290 }
291
292 package PMG::Config::VirusQuarantine;
293
294 use strict;
295 use warnings;
296
297 use base qw(PMG::Config::Base);
298
299 sub type {
300 return 'virusquar';
301 }
302
303 sub properties {
304 return {};
305 }
306
307 sub options {
308 return {
309 lifetime => { optional => 1 },
310 viewimages => { optional => 1 },
311 allowhrefs => { optional => 1 },
312 };
313 }
314
315 package PMG::Config::ClamAV;
316
317 use strict;
318 use warnings;
319
320 use base qw(PMG::Config::Base);
321
322 sub type {
323 return 'clamav';
324 }
325
326 sub properties {
327 return {
328 dbmirror => {
329 description => "ClamAV database mirror server.",
330 type => 'string',
331 default => 'database.clamav.net',
332 },
333 archiveblockencrypted => {
334 description => "Whether to block encrypted archives. Mark encrypted archives as viruses.",
335 type => 'boolean',
336 default => 0,
337 },
338 archivemaxrec => {
339 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.",
340 type => 'integer',
341 minimum => 1,
342 default => 5,
343 },
344 archivemaxfiles => {
345 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.",
346 type => 'integer',
347 minimum => 0,
348 default => 1000,
349 },
350 archivemaxsize => {
351 description => "Files larger than this limit won't be scanned.",
352 type => 'integer',
353 minimum => 1000000,
354 default => 25000000,
355 },
356 maxscansize => {
357 description => "Sets the maximum amount of data to be scanned for each input file.",
358 type => 'integer',
359 minimum => 1000000,
360 default => 100000000,
361 },
362 maxcccount => {
363 description => "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
364 type => 'integer',
365 minimum => 0,
366 default => 0,
367 },
368 safebrowsing => {
369 description => "Enables support for Google Safe Browsing.",
370 type => 'boolean',
371 default => 1
372 },
373 };
374 }
375
376 sub options {
377 return {
378 archiveblockencrypted => { optional => 1 },
379 archivemaxrec => { optional => 1 },
380 archivemaxfiles => { optional => 1 },
381 archivemaxsize => { optional => 1 },
382 maxscansize => { optional => 1 },
383 dbmirror => { optional => 1 },
384 maxcccount => { optional => 1 },
385 safebrowsing => { optional => 1 },
386 };
387 }
388
389 package PMG::Config::Mail;
390
391 use strict;
392 use warnings;
393
394 use PVE::ProcFSTools;
395
396 use base qw(PMG::Config::Base);
397
398 sub type {
399 return 'mail';
400 }
401
402 my $physicalmem = 0;
403 sub physical_memory {
404
405 return $physicalmem if $physicalmem;
406
407 my $info = PVE::ProcFSTools::read_meminfo();
408 my $total = int($info->{memtotal} / (1024*1024));
409
410 return $total;
411 }
412
413 sub get_max_filters {
414 # estimate optimal number of filter servers
415
416 my $max_servers = 5;
417 my $servermem = 120;
418 my $memory = physical_memory();
419 my $add_servers = int(($memory - 512)/$servermem);
420 $max_servers += $add_servers if $add_servers > 0;
421 $max_servers = 40 if $max_servers > 40;
422
423 return $max_servers - 2;
424 }
425
426 sub get_max_smtpd {
427 # estimate optimal number of smtpd daemons
428
429 my $max_servers = 25;
430 my $servermem = 20;
431 my $memory = physical_memory();
432 my $add_servers = int(($memory - 512)/$servermem);
433 $max_servers += $add_servers if $add_servers > 0;
434 $max_servers = 100 if $max_servers > 100;
435 return $max_servers;
436 }
437
438 sub get_max_policy {
439 # estimate optimal number of proxpolicy servers
440 my $max_servers = 2;
441 my $memory = physical_memory();
442 $max_servers = 5 if $memory >= 500;
443 return $max_servers;
444 }
445
446 sub properties {
447 return {
448 int_port => {
449 description => "SMTP port number for outgoing mail (trusted).",
450 type => 'integer',
451 minimum => 1,
452 maximum => 65535,
453 default => 26,
454 },
455 ext_port => {
456 description => "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
457 type => 'integer',
458 minimum => 1,
459 maximum => 65535,
460 default => 25,
461 },
462 relay => {
463 description => "The default mail delivery transport (incoming mails).",
464 type => 'string', format => 'address',
465 },
466 relayport => {
467 description => "SMTP port number for relay host.",
468 type => 'integer',
469 minimum => 1,
470 maximum => 65535,
471 default => 25,
472 },
473 relaynomx => {
474 description => "Disable MX lookups for default relay.",
475 type => 'boolean',
476 default => 0,
477 },
478 smarthost => {
479 description => "When set, all outgoing mails are deliverd to the specified smarthost.",
480 type => 'string', format => 'address',
481 },
482 smarthostport => {
483 description => "SMTP port number for smarthost.",
484 type => 'integer',
485 minimum => 1,
486 maximum => 65535,
487 default => 25,
488 },
489 banner => {
490 description => "ESMTP banner.",
491 type => 'string',
492 maxLength => 1024,
493 default => 'ESMTP Proxmox',
494 },
495 max_filters => {
496 description => "Maximum number of pmg-smtp-filter processes.",
497 type => 'integer',
498 minimum => 3,
499 maximum => 40,
500 default => get_max_filters(),
501 },
502 max_policy => {
503 description => "Maximum number of pmgpolicy processes.",
504 type => 'integer',
505 minimum => 2,
506 maximum => 10,
507 default => get_max_policy(),
508 },
509 max_smtpd_in => {
510 description => "Maximum number of SMTP daemon processes (in).",
511 type => 'integer',
512 minimum => 3,
513 maximum => 100,
514 default => get_max_smtpd(),
515 },
516 max_smtpd_out => {
517 description => "Maximum number of SMTP daemon processes (out).",
518 type => 'integer',
519 minimum => 3,
520 maximum => 100,
521 default => get_max_smtpd(),
522 },
523 conn_count_limit => {
524 description => "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
525 type => 'integer',
526 minimum => 0,
527 default => 50,
528 },
529 conn_rate_limit => {
530 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.",
531 type => 'integer',
532 minimum => 0,
533 default => 0,
534 },
535 message_rate_limit => {
536 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.",
537 type => 'integer',
538 minimum => 0,
539 default => 0,
540 },
541 hide_received => {
542 description => "Hide received header in outgoing mails.",
543 type => 'boolean',
544 default => 0,
545 },
546 maxsize => {
547 description => "Maximum email size. Larger mails are rejected.",
548 type => 'integer',
549 minimum => 1024,
550 default => 1024*1024*10,
551 },
552 dwarning => {
553 description => "SMTP delay warning time (in hours).",
554 type => 'integer',
555 minimum => 0,
556 default => 4,
557 },
558 tls => {
559 description => "Enable TLS.",
560 type => 'boolean',
561 default => 0,
562 },
563 tlslog => {
564 description => "Enable TLS Logging.",
565 type => 'boolean',
566 default => 0,
567 },
568 tlsheader => {
569 description => "Add TLS received header.",
570 type => 'boolean',
571 default => 0,
572 },
573 spf => {
574 description => "Use Sender Policy Framework.",
575 type => 'boolean',
576 default => 1,
577 },
578 greylist => {
579 description => "Use Greylisting.",
580 type => 'boolean',
581 default => 1,
582 },
583 helotests => {
584 description => "Use SMTP HELO tests.",
585 type => 'boolean',
586 default => 0,
587 },
588 rejectunknown => {
589 description => "Reject unknown clients.",
590 type => 'boolean',
591 default => 0,
592 },
593 rejectunknownsender => {
594 description => "Reject unknown senders.",
595 type => 'boolean',
596 default => 0,
597 },
598 verifyreceivers => {
599 description => "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
600 type => 'string',
601 enum => ['450', '550'],
602 },
603 dnsbl_sites => {
604 description => "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
605 type => 'string', format => 'dnsbl-entry-list',
606 },
607 dnsbl_threshold => {
608 description => "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
609 type => 'integer',
610 minimum => 0,
611 default => 1
612 },
613 };
614 }
615
616 sub options {
617 return {
618 int_port => { optional => 1 },
619 ext_port => { optional => 1 },
620 smarthost => { optional => 1 },
621 smarthostport => { optional => 1 },
622 relay => { optional => 1 },
623 relayport => { optional => 1 },
624 relaynomx => { optional => 1 },
625 dwarning => { optional => 1 },
626 max_smtpd_in => { optional => 1 },
627 max_smtpd_out => { optional => 1 },
628 greylist => { optional => 1 },
629 helotests => { optional => 1 },
630 tls => { optional => 1 },
631 tlslog => { optional => 1 },
632 tlsheader => { optional => 1 },
633 spf => { optional => 1 },
634 maxsize => { optional => 1 },
635 banner => { optional => 1 },
636 max_filters => { optional => 1 },
637 max_policy => { optional => 1 },
638 hide_received => { optional => 1 },
639 rejectunknown => { optional => 1 },
640 rejectunknownsender => { optional => 1 },
641 conn_count_limit => { optional => 1 },
642 conn_rate_limit => { optional => 1 },
643 message_rate_limit => { optional => 1 },
644 verifyreceivers => { optional => 1 },
645 dnsbl_sites => { optional => 1 },
646 dnsbl_threshold => { optional => 1 },
647 };
648 }
649
650 package PMG::Config;
651
652 use strict;
653 use warnings;
654 use IO::File;
655 use Data::Dumper;
656 use Template;
657
658 use PVE::SafeSyslog;
659 use PVE::Tools qw($IPV4RE $IPV6RE);
660 use PVE::INotify;
661 use PVE::JSONSchema;
662
663 use PMG::Cluster;
664
665 PMG::Config::Admin->register();
666 PMG::Config::Mail->register();
667 PMG::Config::SpamQuarantine->register();
668 PMG::Config::VirusQuarantine->register();
669 PMG::Config::Spam->register();
670 PMG::Config::ClamAV->register();
671
672 # initialize all plugins
673 PMG::Config::Base->init();
674
675 PVE::JSONSchema::register_format(
676 'transport-domain', \&pmg_verify_transport_domain);
677
678 sub pmg_verify_transport_domain {
679 my ($name, $noerr) = @_;
680
681 # like dns-name, but can contain leading dot
682 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
683
684 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
685 return undef if $noerr;
686 die "value does not look like a valid transport domain\n";
687 }
688 return $name;
689 }
690
691 PVE::JSONSchema::register_format(
692 'transport-domain-or-email', \&pmg_verify_transport_domain_or_email);
693
694 sub pmg_verify_transport_domain_or_email {
695 my ($name, $noerr) = @_;
696
697 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
698
699 # email address
700 if ($name =~ m/^(?:[^\s\/\@]+\@)(${namere}\.)*${namere}$/) {
701 return $name;
702 }
703
704 # like dns-name, but can contain leading dot
705 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
706 return undef if $noerr;
707 die "value does not look like a valid transport domain or email address\n";
708 }
709 return $name;
710 }
711
712 PVE::JSONSchema::register_format(
713 'dnsbl-entry', \&pmg_verify_dnsbl_entry);
714
715 sub pmg_verify_dnsbl_entry {
716 my ($name, $noerr) = @_;
717
718 # like dns-name, but can contain trailing weight: 'domain*<WEIGHT>'
719 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
720
721 if ($name !~ /^(${namere}\.)*${namere}(\*\-?\d+)?$/) {
722 return undef if $noerr;
723 die "value '$name' does not look like a valid dnsbl entry\n";
724 }
725 return $name;
726 }
727
728 sub new {
729 my ($type) = @_;
730
731 my $class = ref($type) || $type;
732
733 my $cfg = PVE::INotify::read_file("pmg.conf");
734
735 return bless $cfg, $class;
736 }
737
738 sub write {
739 my ($self) = @_;
740
741 PVE::INotify::write_file("pmg.conf", $self);
742 }
743
744 my $lockfile = "/var/lock/pmgconfig.lck";
745
746 sub lock_config {
747 my ($code, $errmsg) = @_;
748
749 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
750 if (my $err = $@) {
751 $errmsg ? die "$errmsg: $err" : die $err;
752 }
753 }
754
755 # set section values
756 sub set {
757 my ($self, $section, $key, $value) = @_;
758
759 my $pdata = PMG::Config::Base->private();
760
761 my $plugin = $pdata->{plugins}->{$section};
762 die "no such section '$section'" if !$plugin;
763
764 if (defined($value)) {
765 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
766 $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
767 $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
768 } else {
769 if (defined($self->{ids}->{$section})) {
770 delete $self->{ids}->{$section}->{$key};
771 }
772 }
773
774 return undef;
775 }
776
777 # get section value or default
778 sub get {
779 my ($self, $section, $key, $nodefault) = @_;
780
781 my $pdata = PMG::Config::Base->private();
782 my $pdesc = $pdata->{propertyList}->{$key};
783 die "no such property '$section/$key'\n"
784 if !(defined($pdesc) && defined($pdata->{options}->{$section}) &&
785 defined($pdata->{options}->{$section}->{$key}));
786
787 if (defined($self->{ids}->{$section}) &&
788 defined(my $value = $self->{ids}->{$section}->{$key})) {
789 return $value;
790 }
791
792 return undef if $nodefault;
793
794 return $pdesc->{default};
795 }
796
797 # get a whole section with default value
798 sub get_section {
799 my ($self, $section) = @_;
800
801 my $pdata = PMG::Config::Base->private();
802 return undef if !defined($pdata->{options}->{$section});
803
804 my $res = {};
805
806 foreach my $key (keys %{$pdata->{options}->{$section}}) {
807
808 my $pdesc = $pdata->{propertyList}->{$key};
809
810 if (defined($self->{ids}->{$section}) &&
811 defined(my $value = $self->{ids}->{$section}->{$key})) {
812 $res->{$key} = $value;
813 next;
814 }
815 $res->{$key} = $pdesc->{default};
816 }
817
818 return $res;
819 }
820
821 # get a whole config with default values
822 sub get_config {
823 my ($self) = @_;
824
825 my $pdata = PMG::Config::Base->private();
826
827 my $res = {};
828
829 foreach my $type (keys %{$pdata->{plugins}}) {
830 my $plugin = $pdata->{plugins}->{$type};
831 $res->{$type} = $self->get_section($type);
832 }
833
834 return $res;
835 }
836
837 sub read_pmg_conf {
838 my ($filename, $fh) = @_;
839
840 local $/ = undef; # slurp mode
841
842 my $raw = <$fh> if defined($fh);
843
844 return PMG::Config::Base->parse_config($filename, $raw);
845 }
846
847 sub write_pmg_conf {
848 my ($filename, $fh, $cfg) = @_;
849
850 my $raw = PMG::Config::Base->write_config($filename, $cfg);
851
852 PVE::Tools::safe_print($filename, $fh, $raw);
853 }
854
855 PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
856 \&read_pmg_conf,
857 \&write_pmg_conf,
858 undef, always_call_parser => 1);
859
860 # parsers/writers for other files
861
862 my $domainsfilename = "/etc/pmg/domains";
863
864 sub postmap_pmg_domains {
865 PMG::Utils::run_postmap($domainsfilename);
866 }
867
868 sub read_pmg_domains {
869 my ($filename, $fh) = @_;
870
871 my $domains = {};
872
873 my $comment = '';
874 if (defined($fh)) {
875 while (defined(my $line = <$fh>)) {
876 chomp $line;
877 next if $line =~ m/^\s*$/;
878 if ($line =~ m/^#(.*)\s*$/) {
879 $comment = $1;
880 next;
881 }
882 if ($line =~ m/^(\S+)\s.*$/) {
883 my $domain = $1;
884 $domains->{$domain} = {
885 domain => $domain, comment => $comment };
886 $comment = '';
887 } else {
888 warn "parse error in '$filename': $line\n";
889 $comment = '';
890 }
891 }
892 }
893
894 return $domains;
895 }
896
897 sub write_pmg_domains {
898 my ($filename, $fh, $domains) = @_;
899
900 foreach my $domain (sort keys %$domains) {
901 my $comment = $domains->{$domain}->{comment};
902 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
903 if defined($comment) && $comment !~ m/^\s*$/;
904
905 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
906 }
907 }
908
909 PVE::INotify::register_file('domains', $domainsfilename,
910 \&read_pmg_domains,
911 \&write_pmg_domains,
912 undef, always_call_parser => 1);
913
914 my $mynetworks_filename = "/etc/pmg/mynetworks";
915
916 sub read_pmg_mynetworks {
917 my ($filename, $fh) = @_;
918
919 my $mynetworks = {};
920
921 my $comment = '';
922 if (defined($fh)) {
923 while (defined(my $line = <$fh>)) {
924 chomp $line;
925 next if $line =~ m/^\s*$/;
926 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
927 my ($network, $prefix_size, $comment) = ($1, $2, $3);
928 my $cidr = "$network/${prefix_size}";
929 $mynetworks->{$cidr} = {
930 cidr => $cidr,
931 network_address => $network,
932 prefix_size => $prefix_size,
933 comment => $comment // '',
934 };
935 } else {
936 warn "parse error in '$filename': $line\n";
937 }
938 }
939 }
940
941 return $mynetworks;
942 }
943
944 sub write_pmg_mynetworks {
945 my ($filename, $fh, $mynetworks) = @_;
946
947 foreach my $cidr (sort keys %$mynetworks) {
948 my $data = $mynetworks->{$cidr};
949 my $comment = $data->{comment} // '*';
950 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
951 }
952 }
953
954 PVE::INotify::register_file('mynetworks', $mynetworks_filename,
955 \&read_pmg_mynetworks,
956 \&write_pmg_mynetworks,
957 undef, always_call_parser => 1);
958
959 PVE::JSONSchema::register_format(
960 'tls-policy', \&pmg_verify_tls_policy);
961
962 # TODO: extend to parse attributes of the policy
963 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
964 sub pmg_verify_tls_policy {
965 my ($policy, $noerr) = @_;
966
967 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
968 return undef if $noerr;
969 die "value '$policy' does not look like a valid tls policy\n";
970 }
971 return $policy;
972 }
973
974 PVE::JSONSchema::register_format(
975 'tls-policy-strict', \&pmg_verify_tls_policy_strict);
976
977 sub pmg_verify_tls_policy_strict {
978 my ($policy, $noerr) = @_;
979
980 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
981 return undef if $noerr;
982 die "value '$policy' does not look like a valid tls policy\n";
983 }
984 return $policy;
985 }
986
987 sub read_tls_policy {
988 my ($filename, $fh) = @_;
989
990 return {} if !defined($fh);
991
992 my $tls_policy = {};
993
994 while (defined(my $line = <$fh>)) {
995 chomp $line;
996 next if $line =~ m/^\s*$/;
997 next if $line =~ m/^#(.*)\s*$/;
998
999 my $parse_error = sub {
1000 my ($err) = @_;
1001 die "parse error in '$filename': $line - $err";
1002 };
1003
1004 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1005 my ($domain, $policy) = ($1, $2);
1006
1007 eval {
1008 pmg_verify_transport_domain($domain);
1009 pmg_verify_tls_policy($policy);
1010 };
1011 if (my $err = $@) {
1012 $parse_error->($err);
1013 next;
1014 }
1015
1016 $tls_policy->{$domain} = {
1017 domain => $domain,
1018 policy => $policy,
1019 };
1020 } else {
1021 $parse_error->('wrong format');
1022 }
1023 }
1024
1025 return $tls_policy;
1026 }
1027
1028 sub write_tls_policy {
1029 my ($filename, $fh, $tls_policy) = @_;
1030
1031 return if !$tls_policy;
1032
1033 foreach my $domain (sort keys %$tls_policy) {
1034 my $entry = $tls_policy->{$domain};
1035 PVE::Tools::safe_print(
1036 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1037 }
1038 }
1039
1040 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1041 PVE::INotify::register_file('tls_policy', $tls_policy_map_filename,
1042 \&read_tls_policy,
1043 \&write_tls_policy,
1044 undef, always_call_parser => 1);
1045
1046 sub postmap_tls_policy {
1047 PMG::Utils::run_postmap($tls_policy_map_filename);
1048 }
1049
1050 my $transport_map_filename = "/etc/pmg/transport";
1051
1052 sub postmap_pmg_transport {
1053 PMG::Utils::run_postmap($transport_map_filename);
1054 }
1055
1056 sub read_transport_map {
1057 my ($filename, $fh) = @_;
1058
1059 return [] if !defined($fh);
1060
1061 my $res = {};
1062
1063 my $comment = '';
1064
1065 while (defined(my $line = <$fh>)) {
1066 chomp $line;
1067 next if $line =~ m/^\s*$/;
1068 if ($line =~ m/^#(.*)\s*$/) {
1069 $comment = $1;
1070 next;
1071 }
1072
1073 my $parse_error = sub {
1074 my ($err) = @_;
1075 warn "parse error in '$filename': $line - $err";
1076 $comment = '';
1077 };
1078
1079 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1080 my ($domain, $host, $port) = ($1, $2, $3);
1081
1082 eval { pmg_verify_transport_domain_or_email($domain); };
1083 if (my $err = $@) {
1084 $parse_error->($err);
1085 next;
1086 }
1087 my $use_mx = 1;
1088 if ($host =~ m/^\[(.*)\]$/) {
1089 $host = $1;
1090 $use_mx = 0;
1091 }
1092
1093 eval { PVE::JSONSchema::pve_verify_address($host); };
1094 if (my $err = $@) {
1095 $parse_error->($err);
1096 next;
1097 }
1098
1099 my $data = {
1100 domain => $domain,
1101 host => $host,
1102 port => $port,
1103 use_mx => $use_mx,
1104 comment => $comment,
1105 };
1106 $res->{$domain} = $data;
1107 $comment = '';
1108 } else {
1109 $parse_error->('wrong format');
1110 }
1111 }
1112
1113 return $res;
1114 }
1115
1116 sub write_transport_map {
1117 my ($filename, $fh, $tmap) = @_;
1118
1119 return if !$tmap;
1120
1121 foreach my $domain (sort keys %$tmap) {
1122 my $data = $tmap->{$domain};
1123
1124 my $comment = $data->{comment};
1125 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1126 if defined($comment) && $comment !~ m/^\s*$/;
1127
1128 my $use_mx = $data->{use_mx};
1129 $use_mx = 0 if $data->{host} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1130
1131 if ($use_mx) {
1132 PVE::Tools::safe_print(
1133 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1134 } else {
1135 PVE::Tools::safe_print(
1136 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1137 }
1138 }
1139 }
1140
1141 PVE::INotify::register_file('transport', $transport_map_filename,
1142 \&read_transport_map,
1143 \&write_transport_map,
1144 undef, always_call_parser => 1);
1145
1146 # config file generation using templates
1147
1148 sub get_template_vars {
1149 my ($self) = @_;
1150
1151 my $vars = { pmg => $self->get_config() };
1152
1153 my $nodename = PVE::INotify::nodename();
1154 my $int_ip = PMG::Cluster::remote_node_ip($nodename);
1155 $vars->{ipconfig}->{int_ip} = $int_ip;
1156
1157 my $transportnets = [];
1158
1159 if (my $tmap = PVE::INotify::read_file('transport')) {
1160 foreach my $domain (sort keys %$tmap) {
1161 my $data = $tmap->{$domain};
1162 my $host = $data->{host};
1163 if ($host =~ m/^$IPV4RE$/) {
1164 push @$transportnets, "$host/32";
1165 } elsif ($host =~ m/^$IPV6RE$/) {
1166 push @$transportnets, "[$host]/128";
1167 }
1168 }
1169 }
1170
1171 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
1172
1173 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1174
1175 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
1176 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1177 push @$mynetworks, "[$1]/$2";
1178 } else {
1179 push @$mynetworks, $int_net_cidr;
1180 }
1181 } else {
1182 if ($int_ip =~ m/^$IPV6RE$/) {
1183 push @$mynetworks, "[$int_ip]/128";
1184 } else {
1185 push @$mynetworks, "$int_ip/32";
1186 }
1187 }
1188
1189 my $netlist = PVE::INotify::read_file('mynetworks');
1190 foreach my $cidr (keys %$netlist) {
1191 if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1192 push @$mynetworks, "[$1]/$2";
1193 } else {
1194 push @$mynetworks, $cidr;
1195 }
1196 }
1197
1198 push @$mynetworks, @$transportnets;
1199
1200 # add default relay to mynetworks
1201 if (my $relay = $self->get('mail', 'relay')) {
1202 if ($relay =~ m/^$IPV4RE$/) {
1203 push @$mynetworks, "$relay/32";
1204 } elsif ($relay =~ m/^$IPV6RE$/) {
1205 push @$mynetworks, "[$relay]/128";
1206 } else {
1207 # DNS name - do nothing ?
1208 }
1209 }
1210
1211 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1212
1213 # normalize dnsbl_sites
1214 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1215 if (scalar(@dnsbl_sites)) {
1216 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1217 }
1218
1219 $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
1220
1221 my $usepolicy = 0;
1222 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1223 $self->get('mail', 'spf');
1224 $vars->{postfix}->{usepolicy} = $usepolicy;
1225
1226 if ($int_ip =~ m/^$IPV6RE$/) {
1227 $vars->{postfix}->{int_ip} = "[$int_ip]";
1228 } else {
1229 $vars->{postfix}->{int_ip} = $int_ip;
1230 }
1231
1232 my $resolv = PVE::INotify::read_file('resolvconf');
1233 $vars->{dns}->{hostname} = $nodename;
1234
1235 my $domain = $resolv->{search} // 'localdomain';
1236 $vars->{dns}->{domain} = $domain;
1237
1238 my $wlbr = "$nodename.$domain";
1239 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1240 $wlbr .= " $r"
1241 }
1242 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1243
1244 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1245 eval {
1246 my $uri = URI->new($proxy);
1247 my $host = $uri->host;
1248 my $port = $uri->port // 8080;
1249 if ($host) {
1250 my $data = { host => $host, port => $port };
1251 if (my $ui = $uri->userinfo) {
1252 my ($username, $pw) = split(/:/, $ui, 2);
1253 $data->{username} = $username;
1254 $data->{password} = $pw if defined($pw);
1255 }
1256 $vars->{proxy} = $data;
1257 }
1258 };
1259 warn "parse http_proxy failed - $@" if $@;
1260 }
1261
1262 return $vars;
1263 }
1264
1265 # use one global TT cache
1266 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1267
1268 my $template_toolkit;
1269
1270 sub get_template_toolkit {
1271
1272 return $template_toolkit if $template_toolkit;
1273
1274 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1275
1276 return $template_toolkit;
1277 }
1278
1279 # rewrite file from template
1280 # return true if file has changed
1281 sub rewrite_config_file {
1282 my ($self, $tmplname, $dstfn) = @_;
1283
1284 my $demo = $self->get('admin', 'demo');
1285
1286 if ($demo) {
1287 my $demosrc = "$tmplname.demo";
1288 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
1289 }
1290
1291 my ($perm, $uid, $gid);
1292
1293 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1294 # needed if file contains a HTTPProxyPasswort
1295
1296 $uid = getpwnam('clamav');
1297 $gid = getgrnam('adm');
1298 $perm = 0600;
1299 }
1300
1301 my $tt = get_template_toolkit();
1302
1303 my $vars = $self->get_template_vars();
1304
1305 my $output = '';
1306
1307 $tt->process($tmplname, $vars, \$output) ||
1308 die $tt->error() . "\n";
1309
1310 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1311
1312 return 0 if defined($old) && ($old eq $output); # no change
1313
1314 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1315
1316 if (defined($uid) && defined($gid)) {
1317 chown($uid, $gid, $dstfn);
1318 }
1319
1320 return 1;
1321 }
1322
1323 # rewrite spam configuration
1324 sub rewrite_config_spam {
1325 my ($self) = @_;
1326
1327 my $use_awl = $self->get('spam', 'use_awl');
1328 my $use_bayes = $self->get('spam', 'use_bayes');
1329 my $use_razor = $self->get('spam', 'use_razor');
1330
1331 my $changes = 0;
1332
1333 # delete AW and bayes databases if those features are disabled
1334 if (!$use_awl) {
1335 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1336 }
1337
1338 if (!$use_bayes) {
1339 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1340 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1341 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1342 }
1343
1344 # make sure we have a custom.cf file (else cluster sync fails)
1345 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1346
1347 $changes = 1 if $self->rewrite_config_file(
1348 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1349
1350 $changes = 1 if $self->rewrite_config_file(
1351 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1352
1353 $changes = 1 if $self->rewrite_config_file(
1354 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1355
1356 $changes = 1 if $self->rewrite_config_file(
1357 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1358
1359 if ($use_razor) {
1360 mkdir "/root/.razor";
1361
1362 $changes = 1 if $self->rewrite_config_file(
1363 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1364
1365 if (! -e '/root/.razor/identity') {
1366 eval {
1367 my $timeout = 30;
1368 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1369 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
1370 };
1371 my $err = $@;
1372 syslog('info', "registering razor failed: $err") if $err;
1373 }
1374 }
1375
1376 return $changes;
1377 }
1378
1379 # rewrite ClamAV configuration
1380 sub rewrite_config_clam {
1381 my ($self) = @_;
1382
1383 return $self->rewrite_config_file(
1384 'clamd.conf.in', '/etc/clamav/clamd.conf');
1385 }
1386
1387 sub rewrite_config_freshclam {
1388 my ($self) = @_;
1389
1390 return $self->rewrite_config_file(
1391 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1392 }
1393
1394 sub rewrite_config_postgres {
1395 my ($self) = @_;
1396
1397 my $pgconfdir = "/etc/postgresql/9.6/main";
1398
1399 my $changes = 0;
1400
1401 $changes = 1 if $self->rewrite_config_file(
1402 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1403
1404 $changes = 1 if $self->rewrite_config_file(
1405 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1406
1407 return $changes;
1408 }
1409
1410 # rewrite /root/.forward
1411 sub rewrite_dot_forward {
1412 my ($self) = @_;
1413
1414 my $dstfn = '/root/.forward';
1415
1416 my $email = $self->get('admin', 'email');
1417
1418 my $output = '';
1419 if ($email && $email =~ m/\s*(\S+)\s*/) {
1420 $output = "$1\n";
1421 } else {
1422 # empty .forward does not forward mails (see man local)
1423 }
1424
1425 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1426
1427 return 0 if defined($old) && ($old eq $output); # no change
1428
1429 PVE::Tools::file_set_contents($dstfn, $output);
1430
1431 return 1;
1432 }
1433
1434 my $write_smtp_whitelist = sub {
1435 my ($filename, $data, $action) = @_;
1436
1437 $action = 'OK' if !$action;
1438
1439 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1440 if -f $filename;
1441
1442 my $new = '';
1443 foreach my $k (sort keys %$data) {
1444 $new .= "$k $action\n";
1445 }
1446
1447 return 0 if defined($old) && ($old eq $new); # no change
1448
1449 PVE::Tools::file_set_contents($filename, $new);
1450
1451 PMG::Utils::run_postmap($filename);
1452
1453 return 1;
1454 };
1455
1456 sub rewrite_postfix_whitelist {
1457 my ($rulecache) = @_;
1458
1459 # see man page for regexp_table for postfix regex table format
1460
1461 # we use a hash to avoid duplicate entries in regex tables
1462 my $tolist = {};
1463 my $fromlist = {};
1464 my $clientlist = {};
1465
1466 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1467 my $oclass = ref($obj);
1468 if ($oclass eq 'PMG::RuleDB::Receiver') {
1469 my $addr = PMG::Utils::quote_regex($obj->{address});
1470 $tolist->{"/^$addr\$/"} = 1;
1471 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1472 my $addr = PMG::Utils::quote_regex($obj->{address});
1473 $tolist->{"/^.+\@$addr\$/"} = 1;
1474 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1475 my $addr = $obj->{address};
1476 $addr =~ s|/|\\/|g;
1477 $tolist->{"/^$addr\$/"} = 1;
1478 }
1479 }
1480
1481 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1482 my $oclass = ref($obj);
1483 my $addr = PMG::Utils::quote_regex($obj->{address});
1484 if ($oclass eq 'PMG::RuleDB::EMail') {
1485 my $addr = PMG::Utils::quote_regex($obj->{address});
1486 $fromlist->{"/^$addr\$/"} = 1;
1487 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1488 my $addr = PMG::Utils::quote_regex($obj->{address});
1489 $fromlist->{"/^.+\@$addr\$/"} = 1;
1490 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1491 my $addr = $obj->{address};
1492 $addr =~ s|/|\\/|g;
1493 $fromlist->{"/^$addr\$/"} = 1;
1494 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1495 $clientlist->{$obj->{address}} = 1;
1496 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1497 $clientlist->{$obj->{address}} = 1;
1498 }
1499 }
1500
1501 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1502 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1503 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1504 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1505 };
1506
1507 # rewrite /etc/postfix/*
1508 sub rewrite_config_postfix {
1509 my ($self, $rulecache) = @_;
1510
1511 # make sure we have required files (else postfix start fails)
1512 IO::File->new($transport_map_filename, 'a', 0644);
1513
1514 my $changes = 0;
1515
1516 if ($self->get('mail', 'tls')) {
1517 eval {
1518 PMG::Utils::gen_proxmox_tls_cert();
1519 };
1520 syslog ('info', "generating certificate failed: $@") if $@;
1521 }
1522
1523 $changes = 1 if $self->rewrite_config_file(
1524 'main.cf.in', '/etc/postfix/main.cf');
1525
1526 $changes = 1 if $self->rewrite_config_file(
1527 'master.cf.in', '/etc/postfix/master.cf');
1528
1529 # make sure we have required files (else postfix start fails)
1530 # Note: postmap need a valid /etc/postfix/main.cf configuration
1531 postmap_pmg_domains();
1532 postmap_pmg_transport();
1533 postmap_tls_policy();
1534
1535 rewrite_postfix_whitelist($rulecache) if $rulecache;
1536
1537 # make sure aliases.db is up to date
1538 system('/usr/bin/newaliases');
1539
1540 return $changes;
1541 }
1542
1543 sub rewrite_config {
1544 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1545
1546 $force_restart = {} if ! $force_restart;
1547
1548 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1549 $force_restart->{postfix}) {
1550 PMG::Utils::service_cmd('postfix', 'restart');
1551 }
1552
1553 if ($self->rewrite_dot_forward() && $restart_services) {
1554 # no need to restart anything
1555 }
1556
1557 if ($self->rewrite_config_postgres() && $restart_services) {
1558 # do nothing (too many side effects)?
1559 # does not happen anyways, because config does not change.
1560 }
1561
1562 if (($self->rewrite_config_spam() && $restart_services) ||
1563 $force_restart->{spam}) {
1564 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1565 }
1566
1567 if (($self->rewrite_config_clam() && $restart_services) ||
1568 $force_restart->{clam}) {
1569 PMG::Utils::service_cmd('clamav-daemon', 'restart');
1570 }
1571
1572 if (($self->rewrite_config_freshclam() && $restart_services) ||
1573 $force_restart->{freshclam}) {
1574 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
1575 }
1576 }
1577
1578 1;