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