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