]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/Config.pm
fix #2430: ruledb disclaimer: make separator configurable
[pmg-api.git] / src / 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;
30c74bd2 11use PVE::Network;
7e0e6dbe
DM
12
13use base qw(PVE::SectionConfig);
14
15my $defaultData = {
16 propertyList => {
17 type => { description => "Section type." },
ef6f5dd1 18 section => {
aa995d5d 19 description => "Section ID.",
7e0e6dbe
DM
20 type => 'string', format => 'pve-configid',
21 },
22 },
23};
24
25sub private {
26 return $defaultData;
27}
28
29sub format_section_header {
30 my ($class, $type, $sectionId) = @_;
31
d79b9b0c
DM
32 die "internal error ($type ne $sectionId)" if $type ne $sectionId;
33
34 return "section: $type\n";
7e0e6dbe
DM
35}
36
37
38sub parse_section_header {
39 my ($class, $line) = @_;
40
d79b9b0c
DM
41 if ($line =~ m/^section:\s*(\S+)\s*$/) {
42 my $section = $1;
7e0e6dbe 43 my $errmsg = undef; # set if you want to skip whole section
d79b9b0c 44 eval { PVE::JSONSchema::pve_verify_configid($section); };
7e0e6dbe
DM
45 $errmsg = $@ if $@;
46 my $config = {}; # to return additional attributes
d79b9b0c 47 return ($section, $section, $errmsg, $config);
7e0e6dbe
DM
48 }
49 return undef;
50}
51
ac5d1312 52package PMG::Config::Admin;
7e0e6dbe
DM
53
54use strict;
55use warnings;
56
57use base qw(PMG::Config::Base);
58
59sub type {
ac5d1312 60 return 'admin';
7e0e6dbe
DM
61}
62
63sub properties {
64 return {
44017d49 65 advfilter => {
848fbc11
DC
66 description => "Enable advanced filters for statistic.",
67 verbose_description => <<EODESC,
68Enable advanced filters for statistic.
69
70If 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
72statistic will not contain these active receivers.
73EODESC
44017d49 74 type => 'boolean',
64e64a21 75 default => 0,
44017d49 76 },
7e0e6dbe
DM
77 dailyreport => {
78 description => "Send daily reports.",
79 type => 'boolean',
80 default => 1,
81 },
5a6956b7
DM
82 statlifetime => {
83 description => "User Statistics Lifetime (days)",
84 type => 'integer',
85 default => 7,
86 minimum => 1,
87 },
f62194b2
DM
88 demo => {
89 description => "Demo mode - do not start SMTP filter.",
90 type => 'boolean',
91 default => 0,
92 },
93 email => {
94 description => "Administrator E-Mail address.",
95 type => 'string', format => 'email',
96 default => 'admin@domain.tld',
ac5d1312 97 },
11081cf6
DM
98 http_proxy => {
99 description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
ac5d1312 100 type => 'string',
11081cf6 101 pattern => "http://.*",
ac5d1312 102 },
6ccbc37f 103 avast => {
670ca9b9 104 description => "Use Avast Virus Scanner (/usr/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.",
6ccbc37f
DM
105 type => 'boolean',
106 default => 0,
107 },
9acd4d57
DM
108 clamav => {
109 description => "Use ClamAV Virus Scanner. This is the default virus scanner and is enabled by default.",
110 type => 'boolean',
111 default => 1,
112 },
89d4d155
SI
113 custom_check => {
114 description => "Use Custom Check Script. The script has to take the defined arguments and can return Virus findings or a Spamscore.",
115 type => 'boolean',
116 default => 0,
117 },
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',
122 },
e4b38221
SI
123 dkim_sign => {
124 description => "DKIM sign outbound mails with the configured Selector.",
125 type => 'boolean',
126 default => 0,
127 },
128 dkim_sign_all_mail => {
129 description => "DKIM sign all outgoing mails irrespective of the Envelope From domain.",
130 type => 'boolean',
131 default => 0,
132 },
133 dkim_selector => {
134 description => "Default DKIM selector",
135 type => 'string', format => 'dns-name', #see RFC6376 3.1
136 },
7e0e6dbe
DM
137 };
138}
139
140sub options {
141 return {
44017d49 142 advfilter => { optional => 1 },
6ccbc37f 143 avast => { optional => 1 },
9acd4d57 144 clamav => { optional => 1 },
5a6956b7 145 statlifetime => { optional => 1 },
7e0e6dbe 146 dailyreport => { optional => 1 },
f62194b2 147 demo => { optional => 1 },
3d812daf 148 email => { optional => 1 },
11081cf6 149 http_proxy => { optional => 1 },
89d4d155
SI
150 custom_check => { optional => 1 },
151 custom_check_path => { optional => 1 },
e4b38221
SI
152 dkim_sign => { optional => 1 },
153 dkim_sign_all_mail => { optional => 1 },
154 dkim_selector => { optional => 1 },
7e0e6dbe
DM
155 };
156}
157
158package PMG::Config::Spam;
159
160use strict;
161use warnings;
162
163use base qw(PMG::Config::Base);
164
165sub type {
166 return 'spam';
167}
168
169sub properties {
170 return {
1ccc8e95
DM
171 languages => {
172 description => "This option is used to specify which languages are considered OK for incoming mail.",
173 type => 'string',
174 pattern => '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
175 default => 'all',
176 },
177 use_bayes => {
178 description => "Whether to use the naive-Bayesian-style classifier.",
179 type => 'boolean',
f6ab3f78 180 default => 0,
1ccc8e95 181 },
582cfacf
DM
182 use_awl => {
183 description => "Use the Auto-Whitelist plugin.",
184 type => 'boolean',
f6ab3f78 185 default => 0,
582cfacf
DM
186 },
187 use_razor => {
188 description => "Whether to use Razor2, if it is available.",
189 type => 'boolean',
190 default => 1,
191 },
1ccc8e95
DM
192 wl_bounce_relays => {
193 description => "Whitelist legitimate bounce relays.",
194 type => 'string',
195 },
cda67dee 196 clamav_heuristic_score => {
476591d9 197 description => "Score for ClamAV heuristics (Encrypted Archives/Documents, PhishingScanURLs, ...).",
0d5209bf
DM
198 type => 'integer',
199 minimum => 0,
200 maximum => 1000,
201 default => 3,
202 },
7e0e6dbe
DM
203 bounce_score => {
204 description => "Additional score for bounce mails.",
205 type => 'integer',
206 minimum => 0,
207 maximum => 1000,
208 default => 0,
209 },
f62194b2
DM
210 rbl_checks => {
211 description => "Enable real time blacklists (RBL) checks.",
212 type => 'boolean',
213 default => 1,
214 },
215 maxspamsize => {
216 description => "Maximum size of spam messages in bytes.",
217 type => 'integer',
4d76e24e 218 minimum => 64,
c4bd7694 219 default => 256*1024,
f62194b2 220 },
5f79b3df
SI
221 extract_text => {
222 description => "Extract text from attachments (doc, pdf, rtf, images) and scan for spam.",
223 type => 'boolean',
224 default => 0,
225 },
7e0e6dbe
DM
226 };
227}
228
229sub options {
230 return {
582cfacf
DM
231 use_awl => { optional => 1 },
232 use_razor => { optional => 1 },
1ccc8e95
DM
233 wl_bounce_relays => { optional => 1 },
234 languages => { optional => 1 },
235 use_bayes => { optional => 1 },
cda67dee 236 clamav_heuristic_score => { optional => 1 },
7e0e6dbe 237 bounce_score => { optional => 1 },
f62194b2
DM
238 rbl_checks => { optional => 1 },
239 maxspamsize => { optional => 1 },
5f79b3df 240 extract_text => { optional => 1 },
f62194b2
DM
241 };
242}
243
fc070a06
DM
244package PMG::Config::SpamQuarantine;
245
246use strict;
247use warnings;
248
249use base qw(PMG::Config::Base);
250
251sub type {
252 return 'spamquar';
253}
254
255sub properties {
256 return {
257 lifetime => {
258 description => "Quarantine life time (days)",
259 type => 'integer',
260 minimum => 1,
261 default => 7,
262 },
263 authmode => {
264 description => "Authentication mode to access the quarantine interface. Mode 'ticket' allows login using tickets sent with the daily spam report. Mode 'ldap' requires to login using an LDAP account. Finally, mode 'ldapticket' allows both ways.",
265 type => 'string',
266 enum => [qw(ticket ldap ldapticket)],
267 default => 'ticket',
268 },
269 reportstyle => {
270 description => "Spam report style.",
271 type => 'string',
77f73dc3 272 enum => [qw(none short verbose custom)],
fc070a06
DM
273 default => 'verbose',
274 },
275 viewimages => {
276 description => "Allow to view images.",
277 type => 'boolean',
278 default => 1,
279 },
280 allowhrefs => {
281 description => "Allow to view hyperlinks.",
282 type => 'boolean',
283 default => 1,
1ac4c2e6
DM
284 },
285 hostname => {
c333c6e0 286 description => "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
1ac4c2e6
DM
287 type => 'string', format => 'address',
288 },
c333c6e0
DC
289 port => {
290 description => "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
291 type => 'integer',
292 minimum => 1,
293 maximum => 65535,
294 default => 8006,
295 },
296 protocol => {
297 description => "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
298 type => 'string',
299 enum => [qw(http https)],
300 default => 'https',
301 },
1ac4c2e6
DM
302 mailfrom => {
303 description => "Text for 'From' header in daily spam report mails.",
304 type => 'string',
305 },
f9fb1781
DC
306 quarantinelink => {
307 description => "Enables user self-service for Quarantine Links. Caution: this is accessible without authentication",
308 type => 'boolean',
309 default => 0,
310 },
fc070a06
DM
311 };
312}
313
314sub options {
315 return {
1ac4c2e6
DM
316 mailfrom => { optional => 1 },
317 hostname => { optional => 1 },
fc070a06
DM
318 lifetime => { optional => 1 },
319 authmode => { optional => 1 },
320 reportstyle => { optional => 1 },
321 viewimages => { optional => 1 },
322 allowhrefs => { optional => 1 },
c333c6e0
DC
323 port => { optional => 1 },
324 protocol => { optional => 1 },
f9fb1781 325 quarantinelink => { optional => 1 },
fc070a06
DM
326 };
327}
328
329package PMG::Config::VirusQuarantine;
330
331use strict;
332use warnings;
333
334use base qw(PMG::Config::Base);
335
336sub type {
337 return 'virusquar';
338}
339
340sub properties {
341 return {};
342}
343
344sub options {
345 return {
346 lifetime => { optional => 1 },
347 viewimages => { optional => 1 },
348 allowhrefs => { optional => 1 },
349 };
350}
351
f62194b2
DM
352package PMG::Config::ClamAV;
353
354use strict;
355use warnings;
356
357use base qw(PMG::Config::Base);
358
359sub type {
360 return 'clamav';
361}
362
363sub properties {
364 return {
ac5d1312
DM
365 dbmirror => {
366 description => "ClamAV database mirror server.",
367 type => 'string',
368 default => 'database.clamav.net',
369 },
370 archiveblockencrypted => {
c8415ad3 371 description => "Whether to mark encrypted archives and documents as heuristic virus match. A match does not necessarily result in an immediate block, it just raises the Spam Score by 'clamav_heuristic_score'.",
ac5d1312
DM
372 type => 'boolean',
373 default => 0,
374 },
375 archivemaxrec => {
376 description => "Nested archives are scanned recursively, e.g. if a ZIP archive contains a TAR file, all files within it will also be scanned. This options specifies how deeply the process should be continued. Warning: setting this limit too high may result in severe damage to the system.",
1baec5ab 377 type => 'integer',
ac5d1312
DM
378 minimum => 1,
379 default => 5,
380 },
f62194b2 381 archivemaxfiles => {
ac5d1312 382 description => "Number of files to be scanned within an archive, a document, or any other kind of container. Warning: disabling this limit or setting it too high may result in severe damage to the system.",
f62194b2
DM
383 type => 'integer',
384 minimum => 0,
385 default => 1000,
386 },
ac5d1312 387 archivemaxsize => {
c8415ad3 388 description => "Files larger than this limit (in bytes) won't be scanned.",
ac5d1312
DM
389 type => 'integer',
390 minimum => 1000000,
391 default => 25000000,
392 },
393 maxscansize => {
c8415ad3 394 description => "Sets the maximum amount of data (in bytes) to be scanned for each input file.",
ac5d1312
DM
395 type => 'integer',
396 minimum => 1000000,
397 default => 100000000,
398 },
399 maxcccount => {
400 description => "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
401 type => 'integer',
402 minimum => 0,
403 default => 0,
404 },
476591d9 405 # FIXME: remove for PMG 8.0 - https://blog.clamav.net/2021/04/are-you-still-attempting-to-download.html
9860e592 406 safebrowsing => {
476591d9 407 description => "Enables support for Google Safe Browsing. (deprecated option, will be ignored)",
9860e592 408 type => 'boolean',
476591d9 409 default => 0
9860e592 410 },
ad02f1c5
SI
411 scriptedupdates => {
412 description => "Enables ScriptedUpdates (incremental download of signatures)",
413 type => 'boolean',
fe2090fb 414 default => 1
ad02f1c5 415 },
f62194b2
DM
416 };
417}
418
419sub options {
420 return {
ac5d1312
DM
421 archiveblockencrypted => { optional => 1 },
422 archivemaxrec => { optional => 1 },
f62194b2 423 archivemaxfiles => { optional => 1 },
ac5d1312
DM
424 archivemaxsize => { optional => 1 },
425 maxscansize => { optional => 1 },
426 dbmirror => { optional => 1 },
427 maxcccount => { optional => 1 },
476591d9 428 safebrowsing => { optional => 1 }, # FIXME: remove for PMG 8.0
ad02f1c5 429 scriptedupdates => { optional => 1},
7e0e6dbe
DM
430 };
431}
432
d9dc3c08
DM
433package PMG::Config::Mail;
434
435use strict;
436use warnings;
437
f62194b2
DM
438use PVE::ProcFSTools;
439
d9dc3c08
DM
440use base qw(PMG::Config::Base);
441
442sub type {
443 return 'mail';
444}
445
f62194b2
DM
446my $physicalmem = 0;
447sub physical_memory {
448
449 return $physicalmem if $physicalmem;
450
451 my $info = PVE::ProcFSTools::read_meminfo();
452 my $total = int($info->{memtotal} / (1024*1024));
453
454 return $total;
455}
456
69fa8bc5 457# heuristic for optimal number of smtp-filter servers
f62194b2 458sub get_max_filters {
f62194b2 459 my $max_servers = 5;
e67d8f23 460 my $per_server_memory_usage = 150;
69fa8bc5 461
f62194b2 462 my $memory = physical_memory();
69fa8bc5
TL
463
464 my $base_memory_usage; # the estimated base load of the system
465 if ($memory < 3840) { # 3.75 GiB
e67d8f23
TL
466 my $memory_gb = sprintf('%.1f', $memory/1024.0);
467 my $warn_str = $memory <= 1900 ? 'minimum 2' : 'recommended 4';
468 warn "system memory size of $memory_gb GiB is below the ${warn_str}+ GiB limit!\n";
469
470 $base_memory_usage = int($memory * 0.625); # for small system assume 5/8 for base system
471 $base_memory_usage = 512 if $base_memory_usage < 512;
b0f049b6 472 } else {
e67d8f23 473 $base_memory_usage = 2560; # 2.5 GiB
b0f049b6 474 }
69fa8bc5 475 my $add_servers = int(($memory - $base_memory_usage)/$per_server_memory_usage);
f62194b2
DM
476 $max_servers += $add_servers if $add_servers > 0;
477 $max_servers = 40 if $max_servers > 40;
478
479 return $max_servers - 2;
480}
481
f609bf7f
DM
482sub get_max_smtpd {
483 # estimate optimal number of smtpd daemons
484
485 my $max_servers = 25;
486 my $servermem = 20;
487 my $memory = physical_memory();
488 my $add_servers = int(($memory - 512)/$servermem);
489 $max_servers += $add_servers if $add_servers > 0;
490 $max_servers = 100 if $max_servers > 100;
491 return $max_servers;
492}
493
03907162
DM
494sub get_max_policy {
495 # estimate optimal number of proxpolicy servers
496 my $max_servers = 2;
497 my $memory = physical_memory();
498 $max_servers = 5 if $memory >= 500;
499 return $max_servers;
500}
f609bf7f 501
d9dc3c08
DM
502sub properties {
503 return {
75a20f14
DM
504 int_port => {
505 description => "SMTP port number for outgoing mail (trusted).",
506 type => 'integer',
507 minimum => 1,
508 maximum => 65535,
cb59bcd1 509 default => 26,
75a20f14
DM
510 },
511 ext_port => {
7b19cc5c 512 description => "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
75a20f14
DM
513 type => 'integer',
514 minimum => 1,
515 maximum => 65535,
cb59bcd1 516 default => 25,
75a20f14 517 },
f609bf7f
DM
518 relay => {
519 description => "The default mail delivery transport (incoming mails).",
66af5153 520 type => 'string', format => 'address',
f609bf7f 521 },
10d97956
JZ
522 relayprotocol => {
523 description => "Transport protocol for relay host.",
524 type => 'string',
525 enum => [qw(smtp lmtp)],
526 default => 'smtp',
527 },
f609bf7f 528 relayport => {
c072abb1 529 description => "SMTP/LMTP port number for relay host.",
f609bf7f
DM
530 type => 'integer',
531 minimum => 1,
532 maximum => 65535,
533 default => 25,
534 },
535 relaynomx => {
c072abb1 536 description => "Disable MX lookups for default relay (SMTP only, ignored for LMTP).",
f609bf7f
DM
537 type => 'boolean',
538 default => 0,
539 },
540 smarthost => {
d74fecf5
TL
541 description => "When set, all outgoing mails are deliverd to the specified smarthost."
542 ." (postfix option `default_transport`)",
3bb296d4 543 type => 'string', format => 'address',
f609bf7f 544 },
68b96293 545 smarthostport => {
d74fecf5 546 description => "SMTP port number for smarthost. (postfix option `default_transport`)",
68b96293
DM
547 type => 'integer',
548 minimum => 1,
549 maximum => 65535,
550 default => 25,
551 },
d9dc3c08
DM
552 banner => {
553 description => "ESMTP banner.",
554 type => 'string',
555 maxLength => 1024,
556 default => 'ESMTP Proxmox',
557 },
f62194b2 558 max_filters => {
03907162 559 description => "Maximum number of pmg-smtp-filter processes.",
f62194b2
DM
560 type => 'integer',
561 minimum => 3,
562 maximum => 40,
563 default => get_max_filters(),
564 },
03907162
DM
565 max_policy => {
566 description => "Maximum number of pmgpolicy processes.",
567 type => 'integer',
568 minimum => 2,
569 maximum => 10,
570 default => get_max_policy(),
571 },
f609bf7f
DM
572 max_smtpd_in => {
573 description => "Maximum number of SMTP daemon processes (in).",
574 type => 'integer',
575 minimum => 3,
576 maximum => 100,
577 default => get_max_smtpd(),
578 },
579 max_smtpd_out => {
580 description => "Maximum number of SMTP daemon processes (out).",
581 type => 'integer',
582 minimum => 3,
583 maximum => 100,
584 default => get_max_smtpd(),
585 },
586 conn_count_limit => {
587 description => "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
588 type => 'integer',
589 minimum => 0,
590 default => 50,
591 },
592 conn_rate_limit => {
593 description => "The maximal number of connection attempts any client is allowed to make to this service per minute. To disable this feature, specify a limit of 0.",
594 type => 'integer',
595 minimum => 0,
596 default => 0,
597 },
598 message_rate_limit => {
599 description => "The maximal number of message delivery requests that any client is allowed to make to this service per minute.To disable this feature, specify a limit of 0.",
600 type => 'integer',
601 minimum => 0,
602 default => 0,
603 },
f62194b2
DM
604 hide_received => {
605 description => "Hide received header in outgoing mails.",
606 type => 'boolean',
ac5d1312
DM
607 default => 0,
608 },
f609bf7f 609 maxsize => {
848fbc11 610 description => "Maximum email size. Larger mails are rejected. (postfix option `message_size_limit`)",
ac5d1312
DM
611 type => 'integer',
612 minimum => 1024,
613 default => 1024*1024*10,
f62194b2 614 },
f609bf7f 615 dwarning => {
848fbc11 616 description => "SMTP delay warning time (in hours). (postfix option `delay_warning_time`)",
f609bf7f
DM
617 type => 'integer',
618 minimum => 0,
619 default => 4,
620 },
f609bf7f 621 tls => {
589be6da
DM
622 description => "Enable TLS.",
623 type => 'boolean',
624 default => 0,
625 },
626 tlslog => {
627 description => "Enable TLS Logging.",
628 type => 'boolean',
629 default => 0,
630 },
631 tlsheader => {
632 description => "Add TLS received header.",
f609bf7f
DM
633 type => 'boolean',
634 default => 0,
635 },
636 spf => {
4d76e24e 637 description => "Use Sender Policy Framework.",
f609bf7f
DM
638 type => 'boolean',
639 default => 1,
640 },
641 greylist => {
e159c0fd 642 description => "Use Greylisting for IPv4.",
f609bf7f
DM
643 type => 'boolean',
644 default => 1,
645 },
a01b071c
SI
646 greylistmask4 => {
647 description => "Netmask to apply for greylisting IPv4 hosts",
648 type => 'integer',
649 minimum => 0,
650 maximum => 32,
651 default => 24,
652 },
e159c0fd
SI
653 greylist6 => {
654 description => "Use Greylisting for IPv6.",
655 type => 'boolean',
656 default => 0,
657 },
a01b071c
SI
658 greylistmask6 => {
659 description => "Netmask to apply for greylisting IPv6 hosts",
660 type => 'integer',
661 minimum => 0,
662 maximum => 128,
663 default => 64,
664 },
f609bf7f 665 helotests => {
6ca3fda8 666 description => "Use SMTP HELO tests. (postfix option `smtpd_helo_restrictions`)",
f609bf7f
DM
667 type => 'boolean',
668 default => 0,
669 },
670 rejectunknown => {
848fbc11 671 description => "Reject unknown clients. (postfix option `reject_unknown_client_hostname`)",
f609bf7f
DM
672 type => 'boolean',
673 default => 0,
674 },
675 rejectunknownsender => {
848fbc11 676 description => "Reject unknown senders. (postfix option `reject_unknown_sender_domain`)",
f609bf7f
DM
677 type => 'boolean',
678 default => 0,
679 },
680 verifyreceivers => {
2b2b30d3 681 description => "Enable receiver verification. The value specifies the numerical reply"
d74fecf5
TL
682 ." code when the Postfix SMTP server rejects a recipient address."
683 ." (postfix options `reject_unknown_recipient_domain`, `reject_unverified_recipient`,"
684 ." and `unverified_recipient_reject_code`)",
90822f27
DM
685 type => 'string',
686 enum => ['450', '550'],
f609bf7f
DM
687 },
688 dnsbl_sites => {
d74fecf5 689 description => "Optional list of DNS white/blacklist domains (postfix option `postscreen_dnsbl_sites`).",
a3051049 690 type => 'string', format => 'dnsbl-entry-list',
f609bf7f 691 },
11247512 692 dnsbl_threshold => {
d74fecf5
TL
693 description => "The inclusive lower bound for blocking a remote SMTP client, based on"
694 ." its combined DNSBL score (postfix option `postscreen_dnsbl_threshold`).",
11247512
AP
695 type => 'integer',
696 minimum => 0,
697 default => 1
698 },
e0cbdf9f
SI
699 before_queue_filtering => {
700 description => "Enable before queue filtering by pmg-smtp-filter",
701 type => 'boolean',
702 default => 0
703 },
88a51503
SI
704 ndr_on_block => {
705 description => "Send out NDR when mail gets blocked",
706 type => 'boolean',
707 default => 0
708 },
ceb383cb 709 smtputf8 => {
95373e92 710 description => "Enable SMTPUTF8 support in Postfix and detection for locally generated mail (postfix option `smtputf8_enable`)",
ceb383cb
SI
711 type => 'boolean',
712 default => 1
713 },
479028df
SI
714 'filter-timeout' => {
715 description => "Timeout for the processing of one mail (in seconds) (postfix option"
716 ." `smtpd_proxy_timeout` and `lmtp_data_done_timeout`)",
717 type => 'integer',
718 default => 600,
719 minimum => 2,
720 maximum => 86400
721 },
d9dc3c08
DM
722 };
723}
724
725sub options {
726 return {
75a20f14
DM
727 int_port => { optional => 1 },
728 ext_port => { optional => 1 },
3d9837d9 729 smarthost => { optional => 1 },
68b96293 730 smarthostport => { optional => 1 },
f609bf7f 731 relay => { optional => 1 },
10d97956 732 relayprotocol => { optional => 1 },
f609bf7f
DM
733 relayport => { optional => 1 },
734 relaynomx => { optional => 1 },
735 dwarning => { optional => 1 },
736 max_smtpd_in => { optional => 1 },
737 max_smtpd_out => { optional => 1 },
738 greylist => { optional => 1 },
a01b071c 739 greylistmask4 => { optional => 1 },
e159c0fd 740 greylist6 => { optional => 1 },
a01b071c 741 greylistmask6 => { optional => 1 },
f609bf7f 742 helotests => { optional => 1 },
f609bf7f 743 tls => { optional => 1 },
589be6da
DM
744 tlslog => { optional => 1 },
745 tlsheader => { optional => 1 },
f609bf7f
DM
746 spf => { optional => 1 },
747 maxsize => { optional => 1 },
d9dc3c08 748 banner => { optional => 1 },
f62194b2 749 max_filters => { optional => 1 },
03907162 750 max_policy => { optional => 1 },
f62194b2 751 hide_received => { optional => 1 },
f609bf7f
DM
752 rejectunknown => { optional => 1 },
753 rejectunknownsender => { optional => 1 },
754 conn_count_limit => { optional => 1 },
755 conn_rate_limit => { optional => 1 },
756 message_rate_limit => { optional => 1 },
757 verifyreceivers => { optional => 1 },
758 dnsbl_sites => { optional => 1 },
11247512 759 dnsbl_threshold => { optional => 1 },
e0cbdf9f 760 before_queue_filtering => { optional => 1 },
88a51503 761 ndr_on_block => { optional => 1 },
ceb383cb 762 smtputf8 => { optional => 1 },
479028df 763 'filter-timeout' => { optional => 1 },
d9dc3c08
DM
764 };
765}
d1156caa 766
7e0e6dbe
DM
767package PMG::Config;
768
769use strict;
770use warnings;
9123cab5 771use IO::File;
7e0e6dbe 772use Data::Dumper;
4ccdc564 773use Template;
7e0e6dbe 774
9123cab5 775use PVE::SafeSyslog;
ba323310 776use PVE::Tools qw($IPV4RE $IPV6RE);
7e0e6dbe 777use PVE::INotify;
b86ac4eb 778use PVE::JSONSchema;
7e0e6dbe 779
d1156caa 780use PMG::Cluster;
2005e4b3 781use PMG::Utils;
d1156caa 782
ac5d1312 783PMG::Config::Admin->register();
d9dc3c08 784PMG::Config::Mail->register();
fc070a06
DM
785PMG::Config::SpamQuarantine->register();
786PMG::Config::VirusQuarantine->register();
7e0e6dbe 787PMG::Config::Spam->register();
f62194b2 788PMG::Config::ClamAV->register();
7e0e6dbe
DM
789
790# initialize all plugins
791PMG::Config::Base->init();
792
b86ac4eb
DM
793PVE::JSONSchema::register_format(
794 'transport-domain', \&pmg_verify_transport_domain);
a3051049 795
b86ac4eb
DM
796sub pmg_verify_transport_domain {
797 my ($name, $noerr) = @_;
798
799 # like dns-name, but can contain leading dot
800 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
801
802 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
803 return undef if $noerr;
804 die "value does not look like a valid transport domain\n";
805 }
806 return $name;
807}
f62194b2 808
22c25daf
DM
809PVE::JSONSchema::register_format(
810 'transport-domain-or-email', \&pmg_verify_transport_domain_or_email);
811
812sub pmg_verify_transport_domain_or_email {
813 my ($name, $noerr) = @_;
814
815 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
816
817 # email address
818 if ($name =~ m/^(?:[^\s\/\@]+\@)(${namere}\.)*${namere}$/) {
819 return $name;
820 }
821
822 # like dns-name, but can contain leading dot
823 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
824 return undef if $noerr;
825 die "value does not look like a valid transport domain or email address\n";
826 }
827 return $name;
828}
829
a3051049
DM
830PVE::JSONSchema::register_format(
831 'dnsbl-entry', \&pmg_verify_dnsbl_entry);
832
833sub pmg_verify_dnsbl_entry {
834 my ($name, $noerr) = @_;
835
9f165e3f
SI
836 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
837 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
838 # we don't implement the ';' separated numbers in pattern, because this
839 # breaks at PVE::JSONSchema::split_list
a3051049
DM
840 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
841
9f165e3f
SI
842 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
843 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
844 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
a3051049 845 return undef if $noerr;
2c30ee0d 846 die "value '$name' does not look like a valid dnsbl entry\n";
a3051049
DM
847 }
848 return $name;
849}
850
f62194b2
DM
851sub new {
852 my ($type) = @_;
853
854 my $class = ref($type) || $type;
855
856 my $cfg = PVE::INotify::read_file("pmg.conf");
857
858 return bless $cfg, $class;
859}
860
be6e2db9
DM
861sub write {
862 my ($self) = @_;
863
864 PVE::INotify::write_file("pmg.conf", $self);
865}
866
f21d933c
DM
867my $lockfile = "/var/lock/pmgconfig.lck";
868
869sub lock_config {
870 my ($code, $errmsg) = @_;
871
872 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
873 if (my $err = $@) {
874 $errmsg ? die "$errmsg: $err" : die $err;
875 }
876}
877
062f0498 878# set section values
062f0498
DM
879sub set {
880 my ($self, $section, $key, $value) = @_;
881
882 my $pdata = PMG::Config::Base->private();
883
062f0498
DM
884 my $plugin = $pdata->{plugins}->{$section};
885 die "no such section '$section'" if !$plugin;
886
062f0498
DM
887 if (defined($value)) {
888 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
d79b9b0c
DM
889 $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
890 $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
062f0498 891 } else {
d79b9b0c
DM
892 if (defined($self->{ids}->{$section})) {
893 delete $self->{ids}->{$section}->{$key};
062f0498
DM
894 }
895 }
896
897 return undef;
898}
899
f62194b2 900# get section value or default
f62194b2 901sub get {
11081cf6 902 my ($self, $section, $key, $nodefault) = @_;
f62194b2
DM
903
904 my $pdata = PMG::Config::Base->private();
e3c530d8
TL
905 my $schema = $pdata->{propertyList}->{$key} // die "no schema for property '$section/$key'\n";
906 my $options = $pdata->{options}->{$section} // die "no options for section '$section/$key'\n";
907
3d9837d9 908 die "no such property '$section/$key'\n"
e3c530d8 909 if !(defined($schema) && defined($options) && defined($options->{$key}));
f62194b2 910
e3c530d8
TL
911 my $values = $self->{ids}->{$section};
912 return $values->{$key} if defined($values) && defined($values->{$key});
f62194b2 913
11081cf6
DM
914 return undef if $nodefault;
915
e3c530d8 916 return $schema->{default};
f62194b2
DM
917}
918
1ccc8e95 919# get a whole section with default value
1ccc8e95
DM
920sub get_section {
921 my ($self, $section) = @_;
922
923 my $pdata = PMG::Config::Base->private();
924 return undef if !defined($pdata->{options}->{$section});
925
926 my $res = {};
927
928 foreach my $key (keys %{$pdata->{options}->{$section}}) {
929
930 my $pdesc = $pdata->{propertyList}->{$key};
931
d79b9b0c
DM
932 if (defined($self->{ids}->{$section}) &&
933 defined(my $value = $self->{ids}->{$section}->{$key})) {
1ccc8e95
DM
934 $res->{$key} = $value;
935 next;
936 }
937 $res->{$key} = $pdesc->{default};
938 }
939
940 return $res;
941}
942
be16be07 943# get a whole config with default values
be16be07
DM
944sub get_config {
945 my ($self) = @_;
946
9dab5fe5
DM
947 my $pdata = PMG::Config::Base->private();
948
be16be07
DM
949 my $res = {};
950
9dab5fe5 951 foreach my $type (keys %{$pdata->{plugins}}) {
9dab5fe5
DM
952 my $plugin = $pdata->{plugins}->{$type};
953 $res->{$type} = $self->get_section($type);
be16be07
DM
954 }
955
956 return $res;
957}
958
7e0e6dbe
DM
959sub read_pmg_conf {
960 my ($filename, $fh) = @_;
f62194b2 961
99e7a8b1 962 my $raw;
181ef3f1 963 $raw = do { local $/ = undef; <$fh> } if defined($fh);
7e0e6dbe
DM
964
965 return PMG::Config::Base->parse_config($filename, $raw);
966}
967
968sub write_pmg_conf {
969 my ($filename, $fh, $cfg) = @_;
970
971 my $raw = PMG::Config::Base->write_config($filename, $cfg);
972
973 PVE::Tools::safe_print($filename, $fh, $raw);
974}
975
3278b571 976PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
f62194b2 977 \&read_pmg_conf,
9dfe7c16
DM
978 \&write_pmg_conf,
979 undef, always_call_parser => 1);
7e0e6dbe 980
f609bf7f
DM
981# parsers/writers for other files
982
3278b571 983my $domainsfilename = "/etc/pmg/domains";
f609bf7f 984
c3f4336c
DM
985sub postmap_pmg_domains {
986 PMG::Utils::run_postmap($domainsfilename);
987}
988
f609bf7f
DM
989sub read_pmg_domains {
990 my ($filename, $fh) = @_;
991
b7298186 992 my $domains = {};
f609bf7f 993
b7298186 994 my $comment = '';
f609bf7f
DM
995 if (defined($fh)) {
996 while (defined(my $line = <$fh>)) {
3118b703
DM
997 chomp $line;
998 next if $line =~ m/^\s*$/;
b7298186
DM
999 if ($line =~ m/^#(.*)\s*$/) {
1000 $comment = $1;
1001 next;
1002 }
1003 if ($line =~ m/^(\S+)\s.*$/) {
f609bf7f 1004 my $domain = $1;
b7298186
DM
1005 $domains->{$domain} = {
1006 domain => $domain, comment => $comment };
1007 $comment = '';
3118b703
DM
1008 } else {
1009 warn "parse error in '$filename': $line\n";
1010 $comment = '';
f609bf7f
DM
1011 }
1012 }
1013 }
1014
1015 return $domains;
1016}
1017
1018sub write_pmg_domains {
b7298186
DM
1019 my ($filename, $fh, $domains) = @_;
1020
1021 foreach my $domain (sort keys %$domains) {
1022 my $comment = $domains->{$domain}->{comment};
1023 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1024 if defined($comment) && $comment !~ m/^\s*$/;
f609bf7f 1025
6b31da64 1026 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
f609bf7f
DM
1027 }
1028}
1029
1030PVE::INotify::register_file('domains', $domainsfilename,
1031 \&read_pmg_domains,
1032 \&write_pmg_domains,
1033 undef, always_call_parser => 1);
1034
e4b38221
SI
1035my $dkimdomainsfile = '/etc/pmg/dkim/domains';
1036
1037PVE::INotify::register_file('dkimdomains', $dkimdomainsfile,
1038 \&read_pmg_domains,
1039 \&write_pmg_domains,
1040 undef, always_call_parser => 1);
1041
bef31f06
DM
1042my $mynetworks_filename = "/etc/pmg/mynetworks";
1043
bef31f06
DM
1044sub read_pmg_mynetworks {
1045 my ($filename, $fh) = @_;
1046
1047 my $mynetworks = {};
1048
1049 my $comment = '';
1050 if (defined($fh)) {
1051 while (defined(my $line = <$fh>)) {
1052 chomp $line;
1053 next if $line =~ m/^\s*$/;
1054 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1055 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1056 my $cidr = "$network/${prefix_size}";
30c74bd2 1057 # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
bef31f06
DM
1058 $mynetworks->{$cidr} = {
1059 cidr => $cidr,
1060 network_address => $network,
1061 prefix_size => $prefix_size,
1062 comment => $comment // '',
1063 };
1064 } else {
1065 warn "parse error in '$filename': $line\n";
1066 }
1067 }
1068 }
1069
1070 return $mynetworks;
1071}
1072
1073sub write_pmg_mynetworks {
1074 my ($filename, $fh, $mynetworks) = @_;
1075
1076 foreach my $cidr (sort keys %$mynetworks) {
1077 my $data = $mynetworks->{$cidr};
1078 my $comment = $data->{comment} // '*';
1079 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
1080 }
1081}
1082
1083PVE::INotify::register_file('mynetworks', $mynetworks_filename,
1084 \&read_pmg_mynetworks,
1085 \&write_pmg_mynetworks,
1086 undef, always_call_parser => 1);
1087
1b449731
SI
1088PVE::JSONSchema::register_format(
1089 'tls-policy', \&pmg_verify_tls_policy);
1090
550f4c47
SI
1091# TODO: extend to parse attributes of the policy
1092my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1b449731
SI
1093sub pmg_verify_tls_policy {
1094 my ($policy, $noerr) = @_;
1095
550f4c47 1096 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1b449731
SI
1097 return undef if $noerr;
1098 die "value '$policy' does not look like a valid tls policy\n";
1099 }
1100 return $policy;
1101}
1102
f1a44c5c
DM
1103PVE::JSONSchema::register_format(
1104 'tls-policy-strict', \&pmg_verify_tls_policy_strict);
550f4c47 1105
f1a44c5c
DM
1106sub pmg_verify_tls_policy_strict {
1107 my ($policy, $noerr) = @_;
550f4c47 1108
f1a44c5c
DM
1109 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1110 return undef if $noerr;
1111 die "value '$policy' does not look like a valid tls policy\n";
1112 }
1113 return $policy;
550f4c47
SI
1114}
1115
644487e3
SI
1116PVE::JSONSchema::register_format(
1117 'transport-domain-or-nexthop', \&pmg_verify_transport_domain_or_nexthop);
1118
1119sub pmg_verify_transport_domain_or_nexthop {
1120 my ($name, $noerr) = @_;
1121
1122 if (pmg_verify_transport_domain($name, 1)) {
1123 return $name;
1124 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1125 my $nexthop = $1;
1126 if ($nexthop =~ m/^\[(.*)\]$/) {
1127 $nexthop = $1;
1128 }
1129 return $name if pmg_verify_transport_address($nexthop, 1);
1130 } else {
1131 return undef if $noerr;
1132 die "value does not look like a valid domain or next-hop\n";
1133 }
1134}
1135
1b449731
SI
1136sub read_tls_policy {
1137 my ($filename, $fh) = @_;
1138
1139 return {} if !defined($fh);
1140
1141 my $tls_policy = {};
1142
1143 while (defined(my $line = <$fh>)) {
1144 chomp $line;
1145 next if $line =~ m/^\s*$/;
1146 next if $line =~ m/^#(.*)\s*$/;
1147
1148 my $parse_error = sub {
1149 my ($err) = @_;
ea7ba72d 1150 warn "parse error in '$filename': $line - $err\n";
1b449731
SI
1151 };
1152
1153 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
cce8e372 1154 my ($destination, $policy) = ($1, $2);
1b449731
SI
1155
1156 eval {
cce8e372 1157 pmg_verify_transport_domain_or_nexthop($destination);
1b449731
SI
1158 pmg_verify_tls_policy($policy);
1159 };
1160 if (my $err = $@) {
1161 $parse_error->($err);
1162 next;
1163 }
1164
cce8e372
SI
1165 $tls_policy->{$destination} = {
1166 destination => $destination,
1b449731
SI
1167 policy => $policy,
1168 };
1169 } else {
1170 $parse_error->('wrong format');
1171 }
1172 }
1173
1174 return $tls_policy;
1175}
1176
1177sub write_tls_policy {
1178 my ($filename, $fh, $tls_policy) = @_;
1179
1180 return if !$tls_policy;
1181
cce8e372
SI
1182 foreach my $destination (sort keys %$tls_policy) {
1183 my $entry = $tls_policy->{$destination};
1b449731 1184 PVE::Tools::safe_print(
cce8e372 1185 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1b449731
SI
1186 }
1187}
1188
959aaeba 1189my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1b449731
SI
1190PVE::INotify::register_file('tls_policy', $tls_policy_map_filename,
1191 \&read_tls_policy,
1192 \&write_tls_policy,
1193 undef, always_call_parser => 1);
959aaeba
DM
1194
1195sub postmap_tls_policy {
1196 PMG::Utils::run_postmap($tls_policy_map_filename);
1197}
1198
fbb8db63
CH
1199sub read_tls_inbound_domains {
1200 my ($filename, $fh) = @_;
1201
1202 return {} if !defined($fh);
1203
1204 my $domains = {};
1205
1206 while (defined(my $line = <$fh>)) {
1207 chomp $line;
1208 next if $line =~ m/^\s*$/;
1209 next if $line =~ m/^#(.*)\s*$/;
1210
1211 my $parse_error = sub {
1212 my ($err) = @_;
ea7ba72d 1213 warn "parse error in '$filename': $line - $err\n";
fbb8db63
CH
1214 };
1215
1216 if ($line =~ m/^(\S+) reject_plaintext_session$/) {
1217 my $domain = $1;
1218
1219 eval { pmg_verify_transport_domain($domain) };
1220 if (my $err = $@) {
1221 $parse_error->($err);
1222 next;
1223 }
1224
1225 $domains->{$domain} = 1;
1226 } else {
1227 $parse_error->('wrong format');
1228 }
1229 }
1230
1231 return $domains;
1232}
1233
1234sub write_tls_inbound_domains {
1235 my ($filename, $fh, $domains) = @_;
1236
1237 return if !$domains;
1238
1239 foreach my $domain (sort keys %$domains) {
1240 PVE::Tools::safe_print($filename, $fh, "$domain reject_plaintext_session\n");
1241 }
1242}
1243
1244my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
1245PVE::INotify::register_file('tls_inbound_domains', $tls_inbound_domains_map_filename,
1246 \&read_tls_inbound_domains,
1247 \&write_tls_inbound_domains,
1248 undef, always_call_parser => 1);
1249
1250sub postmap_tls_inbound_domains {
1251 PMG::Utils::run_postmap($tls_inbound_domains_map_filename);
1252}
1253
cd533938 1254my $transport_map_filename = "/etc/pmg/transport";
3546daf0 1255
3118b703
DM
1256sub postmap_pmg_transport {
1257 PMG::Utils::run_postmap($transport_map_filename);
1258}
1259
8e3b0ef1
SI
1260PVE::JSONSchema::register_format(
1261 'transport-address', \&pmg_verify_transport_address);
1262
1263sub pmg_verify_transport_address {
1264 my ($name, $noerr) = @_;
1265
1266 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1267 return $name;
1268 } elsif (PVE::JSONSchema::pve_verify_address($name, 1)) {
1269 return $name;
1270 } else {
1271 return undef if $noerr;
1272 die "value does not look like a valid address\n";
1273 }
1274}
1275
3546daf0
DM
1276sub read_transport_map {
1277 my ($filename, $fh) = @_;
1278
1279 return [] if !defined($fh);
1280
1281 my $res = {};
1282
3118b703 1283 my $comment = '';
b7c49fec 1284
3546daf0
DM
1285 while (defined(my $line = <$fh>)) {
1286 chomp $line;
1287 next if $line =~ m/^\s*$/;
3118b703
DM
1288 if ($line =~ m/^#(.*)\s*$/) {
1289 $comment = $1;
1290 next;
1291 }
3546daf0 1292
b7c49fec
DM
1293 my $parse_error = sub {
1294 my ($err) = @_;
1295 warn "parse error in '$filename': $line - $err";
1296 $comment = '';
1297 };
1298
10d97956
JZ
1299 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1300 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
3546daf0 1301
22c25daf 1302 eval { pmg_verify_transport_domain_or_email($domain); };
b7c49fec
DM
1303 if (my $err = $@) {
1304 $parse_error->($err);
1305 next;
1306 }
53904163 1307 my $use_mx = 1;
3546daf0
DM
1308 if ($host =~ m/^\[(.*)\]$/) {
1309 $host = $1;
53904163 1310 $use_mx = 0;
3546daf0 1311 }
0280ecd4 1312 $use_mx = 0 if ($protocol eq "lmtp");
3546daf0 1313
8e3b0ef1 1314 eval { pmg_verify_transport_address($host); };
b7c49fec
DM
1315 if (my $err = $@) {
1316 $parse_error->($err);
1317 next;
1318 }
1319
3118b703
DM
1320 my $data = {
1321 domain => $domain,
10d97956 1322 protocol => $protocol,
3118b703
DM
1323 host => $host,
1324 port => $port,
53904163 1325 use_mx => $use_mx,
3118b703
DM
1326 comment => $comment,
1327 };
1328 $res->{$domain} = $data;
1329 $comment = '';
1330 } else {
b7c49fec 1331 $parse_error->('wrong format');
3546daf0
DM
1332 }
1333 }
1334
3118b703 1335 return $res;
3546daf0
DM
1336}
1337
cd533938 1338sub write_transport_map {
3546daf0
DM
1339 my ($filename, $fh, $tmap) = @_;
1340
1341 return if !$tmap;
1342
3118b703
DM
1343 foreach my $domain (sort keys %$tmap) {
1344 my $data = $tmap->{$domain};
3546daf0 1345
3118b703
DM
1346 my $comment = $data->{comment};
1347 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1348 if defined($comment) && $comment !~ m/^\s*$/;
1349
256ff1ba 1350 my $bracket_host = !$data->{use_mx};
ba323310 1351
204fd68b 1352 if ($data->{protocol} eq 'lmtp') {
0280ecd4 1353 $bracket_host = 0;
204fd68b 1354 $data->{protocol} .= ":inet";
10d97956 1355 }
8e3b0ef1 1356 $bracket_host = 1 if $data->{host} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
256ff1ba 1357 my $host = $bracket_host ? "[$data->{host}]" : $data->{host};
0280ecd4 1358
256ff1ba 1359 PVE::Tools::safe_print($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
3546daf0
DM
1360 }
1361}
1362
1363PVE::INotify::register_file('transport', $transport_map_filename,
1364 \&read_transport_map,
cd533938 1365 \&write_transport_map,
3546daf0 1366 undef, always_call_parser => 1);
7e0e6dbe 1367
4ccdc564
DM
1368# config file generation using templates
1369
08a1d1d9
SI
1370sub get_host_dns_info {
1371 my ($self) = @_;
1372
1373 my $dnsinfo = {};
1374 my $nodename = PVE::INotify::nodename();
1375
1376 $dnsinfo->{hostname} = $nodename;
1377 my $resolv = PVE::INotify::read_file('resolvconf');
1378
1379 my $domain = $resolv->{search} // 'localdomain';
4091d7d5
SI
1380 # postfix will not parse a hostname with trailing '.'
1381 $domain =~ s/^(.*)\.$/$1/;
08a1d1d9
SI
1382 $dnsinfo->{domain} = $domain;
1383
1384 $dnsinfo->{fqdn} = "$nodename.$domain";
1385
1386 return $dnsinfo;
1387}
1388
07b3face
DM
1389sub get_template_vars {
1390 my ($self) = @_;
4ccdc564
DM
1391
1392 my $vars = { pmg => $self->get_config() };
1393
08a1d1d9
SI
1394 my $dnsinfo = get_host_dns_info();
1395 $vars->{dns} = $dnsinfo;
1396 my $int_ip = PMG::Cluster::remote_node_ip($dnsinfo->{hostname});
f609bf7f 1397 $vars->{ipconfig}->{int_ip} = $int_ip;
f609bf7f 1398
dc21c233
DC
1399 my $transportnets = {};
1400 my $mynetworks = {
1401 '127.0.0.0/8' => 1,
1402 '[::1]/128', => 1,
1403 };
ba323310 1404
5e37e665 1405 if (my $tmap = PVE::INotify::read_file('transport')) {
dc21c233 1406 foreach my $domain (keys %$tmap) {
5e37e665
DM
1407 my $data = $tmap->{$domain};
1408 my $host = $data->{host};
1409 if ($host =~ m/^$IPV4RE$/) {
dc21c233
DC
1410 $transportnets->{"$host/32"} = 1;
1411 $mynetworks->{"$host/32"} = 1;
8e3b0ef1 1412 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
dc21c233
DC
1413 $transportnets->{"[$1]/128"} = 1;
1414 $mynetworks->{"[$1]/128"} = 1;
5e37e665 1415 }
ba323310
DM
1416 }
1417 }
1418
dc21c233 1419 $vars->{postfix}->{transportnets} = join(' ', sort keys %$transportnets);
1e88a529 1420
c48fefb0
TL
1421 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1422 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
1423 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
dc21c233 1424 $mynetworks->{"[$1]/$2"} = 1;
c48fefb0 1425 } else {
dc21c233 1426 $mynetworks->{$int_net_cidr} = 1;
c48fefb0 1427 }
1e88a529 1428 } else {
c48fefb0 1429 if ($int_ip =~ m/^$IPV6RE$/) {
dc21c233 1430 $mynetworks->{"[$int_ip]/128"} = 1;
c48fefb0 1431 } else {
dc21c233 1432 $mynetworks->{"$int_ip/32"} = 1;
c48fefb0 1433 }
1e88a529
DM
1434 }
1435 }
f609bf7f 1436
bef31f06 1437 my $netlist = PVE::INotify::read_file('mynetworks');
dc21c233 1438 foreach my $cidr (keys %$netlist) {
30c74bd2
CH
1439 my $ip = PVE::Network::IP_from_cidr($cidr);
1440
1441 if (!$ip) {
1442 warn "failed to parse mynetworks entry '$cidr', ignoring\n";
1443 } elsif ($ip->version() == 4) {
1444 $mynetworks->{$ip->prefix()} = 1;
136e6c92 1445 } else {
30c74bd2
CH
1446 my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
1447 $mynetworks->{$address} = 1;
136e6c92
DM
1448 }
1449 }
6d473888 1450
f609bf7f
DM
1451 # add default relay to mynetworks
1452 if (my $relay = $self->get('mail', 'relay')) {
ba323310 1453 if ($relay =~ m/^$IPV4RE$/) {
dc21c233 1454 $mynetworks->{"$relay/32"} = 1;
ba323310 1455 } elsif ($relay =~ m/^$IPV6RE$/) {
dc21c233 1456 $mynetworks->{"[$relay]/128"} = 1;
f609bf7f 1457 } else {
66af5153 1458 # DNS name - do nothing ?
f609bf7f
DM
1459 }
1460 }
1461
dc21c233 1462 $vars->{postfix}->{mynetworks} = join(' ', sort keys %$mynetworks);
f609bf7f 1463
20125a71
DM
1464 # normalize dnsbl_sites
1465 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1466 if (scalar(@dnsbl_sites)) {
1467 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1468 }
1469
11247512
AP
1470 $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
1471
f609bf7f
DM
1472 my $usepolicy = 0;
1473 $usepolicy = 1 if $self->get('mail', 'greylist') ||
aa7c3745 1474 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
f609bf7f
DM
1475 $vars->{postfix}->{usepolicy} = $usepolicy;
1476
c48fefb0
TL
1477 if (!defined($int_ip)) {
1478 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1479 $vars->{postfix}->{int_ip} = '127.0.0.1';
1480 } elsif ($int_ip =~ m/^$IPV6RE$/) {
2664d3cb
DM
1481 $vars->{postfix}->{int_ip} = "[$int_ip]";
1482 } else {
1483 $vars->{postfix}->{int_ip} = $int_ip;
1484 }
1485
08a1d1d9 1486 my $wlbr = $dnsinfo->{fqdn};
ed5fa523
DM
1487 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1488 $wlbr .= " $r"
1489 }
1490 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1491
11081cf6
DM
1492 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1493 eval {
1494 my $uri = URI->new($proxy);
1495 my $host = $uri->host;
1496 my $port = $uri->port // 8080;
1497 if ($host) {
1498 my $data = { host => $host, port => $port };
1499 if (my $ui = $uri->userinfo) {
1500 my ($username, $pw) = split(/:/, $ui, 2);
1501 $data->{username} = $username;
1502 $data->{password} = $pw if defined($pw);
1503 }
1504 $vars->{proxy} = $data;
1505 }
1506 };
1507 warn "parse http_proxy failed - $@" if $@;
1508 }
2005e4b3 1509 $vars->{postgres}->{version} = PMG::Utils::get_pg_server_version();
11081cf6 1510
07b3face
DM
1511 return $vars;
1512}
1513
99e7a8b1
TL
1514# reads the $filename and checks if it's equal as the $cmp string passed
1515my sub file_content_equals_str {
1516 my ($filename, $cmp) = @_;
1517
1518 return if !-f $filename;
1519 my $current = PVE::Tools::file_get_contents($filename, 128*1024);
1520 return defined($current) && $current eq $cmp; # no change
1521}
1522
310daf18
DM
1523# use one global TT cache
1524our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1525
1526my $template_toolkit;
1527
1528sub get_template_toolkit {
1529
1530 return $template_toolkit if $template_toolkit;
1531
1532 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1533
1534 return $template_toolkit;
1535}
1536
07b3face
DM
1537# rewrite file from template
1538# return true if file has changed
1539sub rewrite_config_file {
1540 my ($self, $tmplname, $dstfn) = @_;
1541
1542 my $demo = $self->get('admin', 'demo');
1543
07b3face 1544 if ($demo) {
310daf18
DM
1545 my $demosrc = "$tmplname.demo";
1546 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
07b3face
DM
1547 }
1548
c248d69f 1549 my ($perm, $uid, $gid);
07b3face 1550
d1156caa 1551 if ($dstfn eq '/etc/clamav/freshclam.conf') {
07b3face
DM
1552 # needed if file contains a HTTPProxyPasswort
1553
1554 $uid = getpwnam('clamav');
1555 $gid = getgrnam('adm');
1556 $perm = 0600;
1557 }
1558
310daf18 1559 my $tt = get_template_toolkit();
07b3face
DM
1560
1561 my $vars = $self->get_template_vars();
1562
c248d69f 1563 my $output = '';
07b3face 1564
99e7a8b1 1565 $tt->process($tmplname, $vars, \$output) || die $tt->error() . "\n";
07b3face 1566
99e7a8b1 1567 return 0 if file_content_equals_str($dstfn, $output); # no change -> nothing to do
07b3face
DM
1568
1569 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1570
1571 if (defined($uid) && defined($gid)) {
1572 chown($uid, $gid, $dstfn);
1573 }
1574
1575 return 1;
4ccdc564
DM
1576}
1577
9123cab5
DM
1578# rewrite spam configuration
1579sub rewrite_config_spam {
1580 my ($self) = @_;
1581
1582 my $use_awl = $self->get('spam', 'use_awl');
1583 my $use_bayes = $self->get('spam', 'use_bayes');
1584 my $use_razor = $self->get('spam', 'use_razor');
1585
17424665
DM
1586 my $changes = 0;
1587
9123cab5 1588 # delete AW and bayes databases if those features are disabled
17424665
DM
1589 if (!$use_awl) {
1590 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1591 }
1592
9123cab5 1593 if (!$use_bayes) {
17424665
DM
1594 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1595 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1596 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
9123cab5
DM
1597 }
1598
f9d8c305 1599 # make sure we have the custom SA files (else cluster sync fails)
9123cab5 1600 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
f9d8c305 1601 IO::File->new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
9123cab5 1602
17424665
DM
1603 $changes = 1 if $self->rewrite_config_file(
1604 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1605
1606 $changes = 1 if $self->rewrite_config_file(
1607 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1608
1609 $changes = 1 if $self->rewrite_config_file(
1610 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1611
1612 $changes = 1 if $self->rewrite_config_file(
1613 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
9123cab5 1614
b1b20b37
SI
1615 $changes = 1 if $self->rewrite_config_file(
1616 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
1617
1689ee83
SI
1618 $changes = 1 if $self->rewrite_config_file(
1619 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
1620
9123cab5
DM
1621 if ($use_razor) {
1622 mkdir "/root/.razor";
17424665
DM
1623
1624 $changes = 1 if $self->rewrite_config_file(
1625 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1626
9123cab5
DM
1627 if (! -e '/root/.razor/identity') {
1628 eval {
1629 my $timeout = 30;
17424665
DM
1630 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1631 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
9123cab5
DM
1632 };
1633 my $err = $@;
b902c0b8 1634 syslog('info', "registering razor failed: $err") if $err;
9123cab5
DM
1635 }
1636 }
17424665
DM
1637
1638 return $changes;
9123cab5
DM
1639}
1640
ac5d1312
DM
1641# rewrite ClamAV configuration
1642sub rewrite_config_clam {
1643 my ($self) = @_;
1644
17424665
DM
1645 return $self->rewrite_config_file(
1646 'clamd.conf.in', '/etc/clamav/clamd.conf');
1647}
1648
1649sub rewrite_config_freshclam {
1650 my ($self) = @_;
1651
1652 return $self->rewrite_config_file(
1653 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
ac5d1312
DM
1654}
1655
86737f12
DM
1656sub rewrite_config_postgres {
1657 my ($self) = @_;
1658
2005e4b3
SI
1659 my $pg_maj_version = PMG::Utils::get_pg_server_version();
1660 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
86737f12 1661
17424665
DM
1662 my $changes = 0;
1663
1664 $changes = 1 if $self->rewrite_config_file(
1665 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1666
1667 $changes = 1 if $self->rewrite_config_file(
1668 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1669
1670 return $changes;
86737f12
DM
1671}
1672
1673# rewrite /root/.forward
1674sub rewrite_dot_forward {
1675 my ($self) = @_;
1676
c248d69f 1677 my $dstfn = '/root/.forward';
86737f12 1678
0bb9a01a 1679 my $email = $self->get('admin', 'email');
c248d69f 1680
e14fda7a 1681 my $output = '';
86737f12 1682 if ($email && $email =~ m/\s*(\S+)\s*/) {
c248d69f 1683 $output = "$1\n";
86737f12
DM
1684 } else {
1685 # empty .forward does not forward mails (see man local)
1686 }
99e7a8b1 1687 return 0 if file_content_equals_str($dstfn, $output); # no change -> nothing to do
c248d69f
DM
1688
1689 PVE::Tools::file_set_contents($dstfn, $output);
1690
1691 return 1;
86737f12
DM
1692}
1693
d15630a9
DM
1694my $write_smtp_whitelist = sub {
1695 my ($filename, $data, $action) = @_;
1696
1697 $action = 'OK' if !$action;
1698
d15630a9
DM
1699 my $new = '';
1700 foreach my $k (sort keys %$data) {
1701 $new .= "$k $action\n";
1702 }
99e7a8b1 1703 return 0 if file_content_equals_str($filename, $new); # no change -> nothing to do
d15630a9
DM
1704
1705 PVE::Tools::file_set_contents($filename, $new);
1706
1707 PMG::Utils::run_postmap($filename);
1708
1709 return 1;
1710};
1711
f9967a49 1712sub rewrite_postfix_whitelist {
d15630a9
DM
1713 my ($rulecache) = @_;
1714
1715 # see man page for regexp_table for postfix regex table format
1716
1717 # we use a hash to avoid duplicate entries in regex tables
1718 my $tolist = {};
1719 my $fromlist = {};
1720 my $clientlist = {};
1721
1722 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1723 my $oclass = ref($obj);
1724 if ($oclass eq 'PMG::RuleDB::Receiver') {
1725 my $addr = PMG::Utils::quote_regex($obj->{address});
1726 $tolist->{"/^$addr\$/"} = 1;
1727 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1728 my $addr = PMG::Utils::quote_regex($obj->{address});
1729 $tolist->{"/^.+\@$addr\$/"} = 1;
1730 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1731 my $addr = $obj->{address};
1732 $addr =~ s|/|\\/|g;
1733 $tolist->{"/^$addr\$/"} = 1;
1734 }
1735 }
1736
1737 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1738 my $oclass = ref($obj);
1739 my $addr = PMG::Utils::quote_regex($obj->{address});
1740 if ($oclass eq 'PMG::RuleDB::EMail') {
1741 my $addr = PMG::Utils::quote_regex($obj->{address});
1742 $fromlist->{"/^$addr\$/"} = 1;
1743 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1744 my $addr = PMG::Utils::quote_regex($obj->{address});
1745 $fromlist->{"/^.+\@$addr\$/"} = 1;
1746 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1747 my $addr = $obj->{address};
1748 $addr =~ s|/|\\/|g;
1749 $fromlist->{"/^$addr\$/"} = 1;
1750 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1751 $clientlist->{$obj->{address}} = 1;
1752 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1753 $clientlist->{$obj->{address}} = 1;
1754 }
1755 }
1756
1757 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1758 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1759 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1760 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1761};
1762
f609bf7f
DM
1763# rewrite /etc/postfix/*
1764sub rewrite_config_postfix {
d15630a9 1765 my ($self, $rulecache) = @_;
f609bf7f 1766
3546daf0 1767 # make sure we have required files (else postfix start fails)
3546daf0 1768 IO::File->new($transport_map_filename, 'a', 0644);
f609bf7f 1769
17424665
DM
1770 my $changes = 0;
1771
f609bf7f
DM
1772 if ($self->get('mail', 'tls')) {
1773 eval {
bc44eb02 1774 PMG::Utils::gen_proxmox_tls_cert();
f609bf7f 1775 };
b902c0b8 1776 syslog ('info', "generating certificate failed: $@") if $@;
f609bf7f
DM
1777 }
1778
17424665
DM
1779 $changes = 1 if $self->rewrite_config_file(
1780 'main.cf.in', '/etc/postfix/main.cf');
1781
1782 $changes = 1 if $self->rewrite_config_file(
1783 'master.cf.in', '/etc/postfix/master.cf');
1784
a0d4ce8d
DM
1785 # make sure we have required files (else postfix start fails)
1786 # Note: postmap need a valid /etc/postfix/main.cf configuration
1787 postmap_pmg_domains();
1788 postmap_pmg_transport();
1789 postmap_tls_policy();
fbb8db63 1790 postmap_tls_inbound_domains();
a0d4ce8d 1791
f9967a49 1792 rewrite_postfix_whitelist($rulecache) if $rulecache;
d15630a9 1793
f609bf7f
DM
1794 # make sure aliases.db is up to date
1795 system('/usr/bin/newaliases');
17424665
DM
1796
1797 return $changes;
f609bf7f
DM
1798}
1799
e5b31a61
SI
1800#parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1801my $pmg_service_params = {
71c68eba
SI
1802 mail => {
1803 hide_received => 1,
1804 ndr_on_block => 1,
ceb383cb 1805 smtputf8 => 1,
71c68eba 1806 },
e4b38221
SI
1807 admin => {
1808 dkim_selector => 1,
1809 dkim_sign => 1,
1810 dkim_sign_all_mail => 1,
1811 },
e5b31a61
SI
1812};
1813
1814my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1815my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1816
1817sub dump_smtp_filter_config {
1818 my ($self) = @_;
1819
1820 my $conf = '';
1821 my $val;
1822 foreach my $sec (sort keys %$pmg_service_params) {
1823 my $conf_sec = $self->{ids}->{$sec} // {};
1824 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1825 $val = $conf_sec->{$key};
1826 $conf .= "$sec.$key:$val\n" if defined($val);
1827 }
1828 }
1829
1830 return $conf;
1831}
1832
1833sub compare_smtp_filter_config {
1834 my ($self) = @_;
1835
1836 my $ret = 0;
1837 my $old;
1838 eval {
1839 $old = PVE::Tools::file_get_contents($smtp_filter_cfg);
1840 };
1841
1842 if (my $err = $@) {
1843 syslog ('warning', "reloading pmg-smtp-filter: $err");
1844 $ret = 1;
1845 } else {
1846 my $new = $self->dump_smtp_filter_config();
1847 $ret = 1 if $old ne $new;
1848 }
1849
1850 $self->write_smtp_filter_config() if $ret;
1851
1852 return $ret;
1853}
1854
1855# writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1856# on config change
1857sub write_smtp_filter_config {
1858 my ($self) = @_;
1859
1860 PVE::Tools::lock_file($smtp_filter_cfg_lock, undef, sub {
1861 PVE::Tools::file_set_contents($smtp_filter_cfg,
1862 $self->dump_smtp_filter_config());
1863 });
1864
1865 die $@ if $@;
1866}
1867
f983300f 1868sub rewrite_config {
d15630a9 1869 my ($self, $rulecache, $restart_services, $force_restart) = @_;
c248d69f 1870
798df412
DM
1871 $force_restart = {} if ! $force_restart;
1872
e5bd6522
TL
1873 my $log_restart = sub {
1874 syslog ('info', "configuration change detected for '$_[0]', restarting");
1875 };
1876
d15630a9 1877 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
798df412 1878 $force_restart->{postfix}) {
e5bd6522 1879 $log_restart->('postfix');
2473cb81 1880 PMG::Utils::service_cmd('postfix', 'reload');
c248d69f
DM
1881 }
1882
1883 if ($self->rewrite_dot_forward() && $restart_services) {
1884 # no need to restart anything
1885 }
1886
1887 if ($self->rewrite_config_postgres() && $restart_services) {
1888 # do nothing (too many side effects)?
1889 # does not happen anyways, because config does not change.
1890 }
f983300f 1891
798df412
DM
1892 if (($self->rewrite_config_spam() && $restart_services) ||
1893 $force_restart->{spam}) {
e5bd6522 1894 $log_restart->('pmg-smtp-filter');
c248d69f
DM
1895 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1896 }
1897
798df412
DM
1898 if (($self->rewrite_config_clam() && $restart_services) ||
1899 $force_restart->{clam}) {
e5bd6522 1900 $log_restart->('clamav-daemon');
8f87fe74 1901 PMG::Utils::service_cmd('clamav-daemon', 'restart');
c248d69f
DM
1902 }
1903
798df412
DM
1904 if (($self->rewrite_config_freshclam() && $restart_services) ||
1905 $force_restart->{freshclam}) {
e5bd6522 1906 $log_restart->('clamav-freshclam');
8f87fe74 1907 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
c248d69f 1908 }
e5b31a61
SI
1909
1910 if (($self->compare_smtp_filter_config() && $restart_services) ||
1911 $force_restart->{spam}) {
1912 syslog ('info', "scheduled reload for pmg-smtp-filter");
1913 PMG::Utils::reload_smtp_filter();
1914 }
f983300f
DM
1915}
1916
7e0e6dbe 19171;