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