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