]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/Config.pm
bump version to 7.3-1
[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 },
ceb383cb
SI
685 smtputf8 => {
686 description => "Enable SMTPUTF8 support in Postfix and detection for locally generated mail",
687 type => 'boolean',
688 default => 1
689 },
d9dc3c08
DM
690 };
691}
692
693sub options {
694 return {
75a20f14
DM
695 int_port => { optional => 1 },
696 ext_port => { optional => 1 },
3d9837d9 697 smarthost => { optional => 1 },
68b96293 698 smarthostport => { optional => 1 },
f609bf7f 699 relay => { optional => 1 },
10d97956 700 relayprotocol => { optional => 1 },
f609bf7f
DM
701 relayport => { optional => 1 },
702 relaynomx => { optional => 1 },
703 dwarning => { optional => 1 },
704 max_smtpd_in => { optional => 1 },
705 max_smtpd_out => { optional => 1 },
706 greylist => { optional => 1 },
a01b071c 707 greylistmask4 => { optional => 1 },
e159c0fd 708 greylist6 => { optional => 1 },
a01b071c 709 greylistmask6 => { optional => 1 },
f609bf7f 710 helotests => { optional => 1 },
f609bf7f 711 tls => { optional => 1 },
589be6da
DM
712 tlslog => { optional => 1 },
713 tlsheader => { optional => 1 },
f609bf7f
DM
714 spf => { optional => 1 },
715 maxsize => { optional => 1 },
d9dc3c08 716 banner => { optional => 1 },
f62194b2 717 max_filters => { optional => 1 },
03907162 718 max_policy => { optional => 1 },
f62194b2 719 hide_received => { optional => 1 },
f609bf7f
DM
720 rejectunknown => { optional => 1 },
721 rejectunknownsender => { optional => 1 },
722 conn_count_limit => { optional => 1 },
723 conn_rate_limit => { optional => 1 },
724 message_rate_limit => { optional => 1 },
725 verifyreceivers => { optional => 1 },
726 dnsbl_sites => { optional => 1 },
11247512 727 dnsbl_threshold => { optional => 1 },
e0cbdf9f 728 before_queue_filtering => { optional => 1 },
88a51503 729 ndr_on_block => { optional => 1 },
ceb383cb 730 smtputf8 => { optional => 1 },
d9dc3c08
DM
731 };
732}
d1156caa 733
7e0e6dbe
DM
734package PMG::Config;
735
736use strict;
737use warnings;
9123cab5 738use IO::File;
7e0e6dbe 739use Data::Dumper;
4ccdc564 740use Template;
7e0e6dbe 741
9123cab5 742use PVE::SafeSyslog;
ba323310 743use PVE::Tools qw($IPV4RE $IPV6RE);
7e0e6dbe 744use PVE::INotify;
b86ac4eb 745use PVE::JSONSchema;
7e0e6dbe 746
d1156caa 747use PMG::Cluster;
2005e4b3 748use PMG::Utils;
d1156caa 749
ac5d1312 750PMG::Config::Admin->register();
d9dc3c08 751PMG::Config::Mail->register();
fc070a06
DM
752PMG::Config::SpamQuarantine->register();
753PMG::Config::VirusQuarantine->register();
7e0e6dbe 754PMG::Config::Spam->register();
f62194b2 755PMG::Config::ClamAV->register();
7e0e6dbe
DM
756
757# initialize all plugins
758PMG::Config::Base->init();
759
b86ac4eb
DM
760PVE::JSONSchema::register_format(
761 'transport-domain', \&pmg_verify_transport_domain);
a3051049 762
b86ac4eb
DM
763sub pmg_verify_transport_domain {
764 my ($name, $noerr) = @_;
765
766 # like dns-name, but can contain leading dot
767 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
768
769 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
770 return undef if $noerr;
771 die "value does not look like a valid transport domain\n";
772 }
773 return $name;
774}
f62194b2 775
22c25daf
DM
776PVE::JSONSchema::register_format(
777 'transport-domain-or-email', \&pmg_verify_transport_domain_or_email);
778
779sub pmg_verify_transport_domain_or_email {
780 my ($name, $noerr) = @_;
781
782 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
783
784 # email address
785 if ($name =~ m/^(?:[^\s\/\@]+\@)(${namere}\.)*${namere}$/) {
786 return $name;
787 }
788
789 # like dns-name, but can contain leading dot
790 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
791 return undef if $noerr;
792 die "value does not look like a valid transport domain or email address\n";
793 }
794 return $name;
795}
796
a3051049
DM
797PVE::JSONSchema::register_format(
798 'dnsbl-entry', \&pmg_verify_dnsbl_entry);
799
800sub pmg_verify_dnsbl_entry {
801 my ($name, $noerr) = @_;
802
9f165e3f
SI
803 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
804 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
805 # we don't implement the ';' separated numbers in pattern, because this
806 # breaks at PVE::JSONSchema::split_list
a3051049
DM
807 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
808
9f165e3f
SI
809 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
810 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
811 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
a3051049 812 return undef if $noerr;
2c30ee0d 813 die "value '$name' does not look like a valid dnsbl entry\n";
a3051049
DM
814 }
815 return $name;
816}
817
f62194b2
DM
818sub new {
819 my ($type) = @_;
820
821 my $class = ref($type) || $type;
822
823 my $cfg = PVE::INotify::read_file("pmg.conf");
824
825 return bless $cfg, $class;
826}
827
be6e2db9
DM
828sub write {
829 my ($self) = @_;
830
831 PVE::INotify::write_file("pmg.conf", $self);
832}
833
f21d933c
DM
834my $lockfile = "/var/lock/pmgconfig.lck";
835
836sub lock_config {
837 my ($code, $errmsg) = @_;
838
839 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
840 if (my $err = $@) {
841 $errmsg ? die "$errmsg: $err" : die $err;
842 }
843}
844
062f0498 845# set section values
062f0498
DM
846sub set {
847 my ($self, $section, $key, $value) = @_;
848
849 my $pdata = PMG::Config::Base->private();
850
062f0498
DM
851 my $plugin = $pdata->{plugins}->{$section};
852 die "no such section '$section'" if !$plugin;
853
062f0498
DM
854 if (defined($value)) {
855 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
d79b9b0c
DM
856 $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
857 $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
062f0498 858 } else {
d79b9b0c
DM
859 if (defined($self->{ids}->{$section})) {
860 delete $self->{ids}->{$section}->{$key};
062f0498
DM
861 }
862 }
863
864 return undef;
865}
866
f62194b2 867# get section value or default
f62194b2 868sub get {
11081cf6 869 my ($self, $section, $key, $nodefault) = @_;
f62194b2
DM
870
871 my $pdata = PMG::Config::Base->private();
f62194b2 872 my $pdesc = $pdata->{propertyList}->{$key};
3d9837d9
DM
873 die "no such property '$section/$key'\n"
874 if !(defined($pdesc) && defined($pdata->{options}->{$section}) &&
875 defined($pdata->{options}->{$section}->{$key}));
f62194b2 876
d79b9b0c
DM
877 if (defined($self->{ids}->{$section}) &&
878 defined(my $value = $self->{ids}->{$section}->{$key})) {
f62194b2 879 return $value;
1ccc8e95 880 }
f62194b2 881
11081cf6
DM
882 return undef if $nodefault;
883
f62194b2
DM
884 return $pdesc->{default};
885}
886
1ccc8e95 887# get a whole section with default value
1ccc8e95
DM
888sub get_section {
889 my ($self, $section) = @_;
890
891 my $pdata = PMG::Config::Base->private();
892 return undef if !defined($pdata->{options}->{$section});
893
894 my $res = {};
895
896 foreach my $key (keys %{$pdata->{options}->{$section}}) {
897
898 my $pdesc = $pdata->{propertyList}->{$key};
899
d79b9b0c
DM
900 if (defined($self->{ids}->{$section}) &&
901 defined(my $value = $self->{ids}->{$section}->{$key})) {
1ccc8e95
DM
902 $res->{$key} = $value;
903 next;
904 }
905 $res->{$key} = $pdesc->{default};
906 }
907
908 return $res;
909}
910
be16be07 911# get a whole config with default values
be16be07
DM
912sub get_config {
913 my ($self) = @_;
914
9dab5fe5
DM
915 my $pdata = PMG::Config::Base->private();
916
be16be07
DM
917 my $res = {};
918
9dab5fe5 919 foreach my $type (keys %{$pdata->{plugins}}) {
9dab5fe5
DM
920 my $plugin = $pdata->{plugins}->{$type};
921 $res->{$type} = $self->get_section($type);
be16be07
DM
922 }
923
924 return $res;
925}
926
7e0e6dbe
DM
927sub read_pmg_conf {
928 my ($filename, $fh) = @_;
f62194b2 929
7e0e6dbe 930 local $/ = undef; # slurp mode
f62194b2 931
99e7a8b1
TL
932 my $raw;
933 $raw = <$fh> if defined($fh);
7e0e6dbe
DM
934
935 return PMG::Config::Base->parse_config($filename, $raw);
936}
937
938sub write_pmg_conf {
939 my ($filename, $fh, $cfg) = @_;
940
941 my $raw = PMG::Config::Base->write_config($filename, $cfg);
942
943 PVE::Tools::safe_print($filename, $fh, $raw);
944}
945
3278b571 946PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
f62194b2 947 \&read_pmg_conf,
9dfe7c16
DM
948 \&write_pmg_conf,
949 undef, always_call_parser => 1);
7e0e6dbe 950
f609bf7f
DM
951# parsers/writers for other files
952
3278b571 953my $domainsfilename = "/etc/pmg/domains";
f609bf7f 954
c3f4336c
DM
955sub postmap_pmg_domains {
956 PMG::Utils::run_postmap($domainsfilename);
957}
958
f609bf7f
DM
959sub read_pmg_domains {
960 my ($filename, $fh) = @_;
961
b7298186 962 my $domains = {};
f609bf7f 963
b7298186 964 my $comment = '';
f609bf7f
DM
965 if (defined($fh)) {
966 while (defined(my $line = <$fh>)) {
3118b703
DM
967 chomp $line;
968 next if $line =~ m/^\s*$/;
b7298186
DM
969 if ($line =~ m/^#(.*)\s*$/) {
970 $comment = $1;
971 next;
972 }
973 if ($line =~ m/^(\S+)\s.*$/) {
f609bf7f 974 my $domain = $1;
b7298186
DM
975 $domains->{$domain} = {
976 domain => $domain, comment => $comment };
977 $comment = '';
3118b703
DM
978 } else {
979 warn "parse error in '$filename': $line\n";
980 $comment = '';
f609bf7f
DM
981 }
982 }
983 }
984
985 return $domains;
986}
987
988sub write_pmg_domains {
b7298186
DM
989 my ($filename, $fh, $domains) = @_;
990
991 foreach my $domain (sort keys %$domains) {
992 my $comment = $domains->{$domain}->{comment};
993 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
994 if defined($comment) && $comment !~ m/^\s*$/;
f609bf7f 995
6b31da64 996 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
f609bf7f
DM
997 }
998}
999
1000PVE::INotify::register_file('domains', $domainsfilename,
1001 \&read_pmg_domains,
1002 \&write_pmg_domains,
1003 undef, always_call_parser => 1);
1004
e4b38221
SI
1005my $dkimdomainsfile = '/etc/pmg/dkim/domains';
1006
1007PVE::INotify::register_file('dkimdomains', $dkimdomainsfile,
1008 \&read_pmg_domains,
1009 \&write_pmg_domains,
1010 undef, always_call_parser => 1);
1011
bef31f06
DM
1012my $mynetworks_filename = "/etc/pmg/mynetworks";
1013
bef31f06
DM
1014sub read_pmg_mynetworks {
1015 my ($filename, $fh) = @_;
1016
1017 my $mynetworks = {};
1018
1019 my $comment = '';
1020 if (defined($fh)) {
1021 while (defined(my $line = <$fh>)) {
1022 chomp $line;
1023 next if $line =~ m/^\s*$/;
1024 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1025 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1026 my $cidr = "$network/${prefix_size}";
30c74bd2 1027 # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
bef31f06
DM
1028 $mynetworks->{$cidr} = {
1029 cidr => $cidr,
1030 network_address => $network,
1031 prefix_size => $prefix_size,
1032 comment => $comment // '',
1033 };
1034 } else {
1035 warn "parse error in '$filename': $line\n";
1036 }
1037 }
1038 }
1039
1040 return $mynetworks;
1041}
1042
1043sub write_pmg_mynetworks {
1044 my ($filename, $fh, $mynetworks) = @_;
1045
1046 foreach my $cidr (sort keys %$mynetworks) {
1047 my $data = $mynetworks->{$cidr};
1048 my $comment = $data->{comment} // '*';
1049 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
1050 }
1051}
1052
1053PVE::INotify::register_file('mynetworks', $mynetworks_filename,
1054 \&read_pmg_mynetworks,
1055 \&write_pmg_mynetworks,
1056 undef, always_call_parser => 1);
1057
1b449731
SI
1058PVE::JSONSchema::register_format(
1059 'tls-policy', \&pmg_verify_tls_policy);
1060
550f4c47
SI
1061# TODO: extend to parse attributes of the policy
1062my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1b449731
SI
1063sub pmg_verify_tls_policy {
1064 my ($policy, $noerr) = @_;
1065
550f4c47 1066 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1b449731
SI
1067 return undef if $noerr;
1068 die "value '$policy' does not look like a valid tls policy\n";
1069 }
1070 return $policy;
1071}
1072
f1a44c5c
DM
1073PVE::JSONSchema::register_format(
1074 'tls-policy-strict', \&pmg_verify_tls_policy_strict);
550f4c47 1075
f1a44c5c
DM
1076sub pmg_verify_tls_policy_strict {
1077 my ($policy, $noerr) = @_;
550f4c47 1078
f1a44c5c
DM
1079 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1080 return undef if $noerr;
1081 die "value '$policy' does not look like a valid tls policy\n";
1082 }
1083 return $policy;
550f4c47
SI
1084}
1085
644487e3
SI
1086PVE::JSONSchema::register_format(
1087 'transport-domain-or-nexthop', \&pmg_verify_transport_domain_or_nexthop);
1088
1089sub pmg_verify_transport_domain_or_nexthop {
1090 my ($name, $noerr) = @_;
1091
1092 if (pmg_verify_transport_domain($name, 1)) {
1093 return $name;
1094 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1095 my $nexthop = $1;
1096 if ($nexthop =~ m/^\[(.*)\]$/) {
1097 $nexthop = $1;
1098 }
1099 return $name if pmg_verify_transport_address($nexthop, 1);
1100 } else {
1101 return undef if $noerr;
1102 die "value does not look like a valid domain or next-hop\n";
1103 }
1104}
1105
1b449731
SI
1106sub read_tls_policy {
1107 my ($filename, $fh) = @_;
1108
1109 return {} if !defined($fh);
1110
1111 my $tls_policy = {};
1112
1113 while (defined(my $line = <$fh>)) {
1114 chomp $line;
1115 next if $line =~ m/^\s*$/;
1116 next if $line =~ m/^#(.*)\s*$/;
1117
1118 my $parse_error = sub {
1119 my ($err) = @_;
ea7ba72d 1120 warn "parse error in '$filename': $line - $err\n";
1b449731
SI
1121 };
1122
1123 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
cce8e372 1124 my ($destination, $policy) = ($1, $2);
1b449731
SI
1125
1126 eval {
cce8e372 1127 pmg_verify_transport_domain_or_nexthop($destination);
1b449731
SI
1128 pmg_verify_tls_policy($policy);
1129 };
1130 if (my $err = $@) {
1131 $parse_error->($err);
1132 next;
1133 }
1134
cce8e372
SI
1135 $tls_policy->{$destination} = {
1136 destination => $destination,
1b449731
SI
1137 policy => $policy,
1138 };
1139 } else {
1140 $parse_error->('wrong format');
1141 }
1142 }
1143
1144 return $tls_policy;
1145}
1146
1147sub write_tls_policy {
1148 my ($filename, $fh, $tls_policy) = @_;
1149
1150 return if !$tls_policy;
1151
cce8e372
SI
1152 foreach my $destination (sort keys %$tls_policy) {
1153 my $entry = $tls_policy->{$destination};
1b449731 1154 PVE::Tools::safe_print(
cce8e372 1155 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1b449731
SI
1156 }
1157}
1158
959aaeba 1159my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1b449731
SI
1160PVE::INotify::register_file('tls_policy', $tls_policy_map_filename,
1161 \&read_tls_policy,
1162 \&write_tls_policy,
1163 undef, always_call_parser => 1);
959aaeba
DM
1164
1165sub postmap_tls_policy {
1166 PMG::Utils::run_postmap($tls_policy_map_filename);
1167}
1168
fbb8db63
CH
1169sub read_tls_inbound_domains {
1170 my ($filename, $fh) = @_;
1171
1172 return {} if !defined($fh);
1173
1174 my $domains = {};
1175
1176 while (defined(my $line = <$fh>)) {
1177 chomp $line;
1178 next if $line =~ m/^\s*$/;
1179 next if $line =~ m/^#(.*)\s*$/;
1180
1181 my $parse_error = sub {
1182 my ($err) = @_;
ea7ba72d 1183 warn "parse error in '$filename': $line - $err\n";
fbb8db63
CH
1184 };
1185
1186 if ($line =~ m/^(\S+) reject_plaintext_session$/) {
1187 my $domain = $1;
1188
1189 eval { pmg_verify_transport_domain($domain) };
1190 if (my $err = $@) {
1191 $parse_error->($err);
1192 next;
1193 }
1194
1195 $domains->{$domain} = 1;
1196 } else {
1197 $parse_error->('wrong format');
1198 }
1199 }
1200
1201 return $domains;
1202}
1203
1204sub write_tls_inbound_domains {
1205 my ($filename, $fh, $domains) = @_;
1206
1207 return if !$domains;
1208
1209 foreach my $domain (sort keys %$domains) {
1210 PVE::Tools::safe_print($filename, $fh, "$domain reject_plaintext_session\n");
1211 }
1212}
1213
1214my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
1215PVE::INotify::register_file('tls_inbound_domains', $tls_inbound_domains_map_filename,
1216 \&read_tls_inbound_domains,
1217 \&write_tls_inbound_domains,
1218 undef, always_call_parser => 1);
1219
1220sub postmap_tls_inbound_domains {
1221 PMG::Utils::run_postmap($tls_inbound_domains_map_filename);
1222}
1223
cd533938 1224my $transport_map_filename = "/etc/pmg/transport";
3546daf0 1225
3118b703
DM
1226sub postmap_pmg_transport {
1227 PMG::Utils::run_postmap($transport_map_filename);
1228}
1229
8e3b0ef1
SI
1230PVE::JSONSchema::register_format(
1231 'transport-address', \&pmg_verify_transport_address);
1232
1233sub pmg_verify_transport_address {
1234 my ($name, $noerr) = @_;
1235
1236 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1237 return $name;
1238 } elsif (PVE::JSONSchema::pve_verify_address($name, 1)) {
1239 return $name;
1240 } else {
1241 return undef if $noerr;
1242 die "value does not look like a valid address\n";
1243 }
1244}
1245
3546daf0
DM
1246sub read_transport_map {
1247 my ($filename, $fh) = @_;
1248
1249 return [] if !defined($fh);
1250
1251 my $res = {};
1252
3118b703 1253 my $comment = '';
b7c49fec 1254
3546daf0
DM
1255 while (defined(my $line = <$fh>)) {
1256 chomp $line;
1257 next if $line =~ m/^\s*$/;
3118b703
DM
1258 if ($line =~ m/^#(.*)\s*$/) {
1259 $comment = $1;
1260 next;
1261 }
3546daf0 1262
b7c49fec
DM
1263 my $parse_error = sub {
1264 my ($err) = @_;
1265 warn "parse error in '$filename': $line - $err";
1266 $comment = '';
1267 };
1268
10d97956
JZ
1269 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1270 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
3546daf0 1271
22c25daf 1272 eval { pmg_verify_transport_domain_or_email($domain); };
b7c49fec
DM
1273 if (my $err = $@) {
1274 $parse_error->($err);
1275 next;
1276 }
53904163 1277 my $use_mx = 1;
3546daf0
DM
1278 if ($host =~ m/^\[(.*)\]$/) {
1279 $host = $1;
53904163 1280 $use_mx = 0;
3546daf0 1281 }
0280ecd4 1282 $use_mx = 0 if ($protocol eq "lmtp");
3546daf0 1283
8e3b0ef1 1284 eval { pmg_verify_transport_address($host); };
b7c49fec
DM
1285 if (my $err = $@) {
1286 $parse_error->($err);
1287 next;
1288 }
1289
3118b703
DM
1290 my $data = {
1291 domain => $domain,
10d97956 1292 protocol => $protocol,
3118b703
DM
1293 host => $host,
1294 port => $port,
53904163 1295 use_mx => $use_mx,
3118b703
DM
1296 comment => $comment,
1297 };
1298 $res->{$domain} = $data;
1299 $comment = '';
1300 } else {
b7c49fec 1301 $parse_error->('wrong format');
3546daf0
DM
1302 }
1303 }
1304
3118b703 1305 return $res;
3546daf0
DM
1306}
1307
cd533938 1308sub write_transport_map {
3546daf0
DM
1309 my ($filename, $fh, $tmap) = @_;
1310
1311 return if !$tmap;
1312
3118b703
DM
1313 foreach my $domain (sort keys %$tmap) {
1314 my $data = $tmap->{$domain};
3546daf0 1315
3118b703
DM
1316 my $comment = $data->{comment};
1317 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1318 if defined($comment) && $comment !~ m/^\s*$/;
1319
256ff1ba 1320 my $bracket_host = !$data->{use_mx};
ba323310 1321
204fd68b 1322 if ($data->{protocol} eq 'lmtp') {
0280ecd4 1323 $bracket_host = 0;
204fd68b 1324 $data->{protocol} .= ":inet";
10d97956 1325 }
8e3b0ef1 1326 $bracket_host = 1 if $data->{host} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
256ff1ba 1327 my $host = $bracket_host ? "[$data->{host}]" : $data->{host};
0280ecd4 1328
256ff1ba 1329 PVE::Tools::safe_print($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
3546daf0
DM
1330 }
1331}
1332
1333PVE::INotify::register_file('transport', $transport_map_filename,
1334 \&read_transport_map,
cd533938 1335 \&write_transport_map,
3546daf0 1336 undef, always_call_parser => 1);
7e0e6dbe 1337
4ccdc564
DM
1338# config file generation using templates
1339
08a1d1d9
SI
1340sub get_host_dns_info {
1341 my ($self) = @_;
1342
1343 my $dnsinfo = {};
1344 my $nodename = PVE::INotify::nodename();
1345
1346 $dnsinfo->{hostname} = $nodename;
1347 my $resolv = PVE::INotify::read_file('resolvconf');
1348
1349 my $domain = $resolv->{search} // 'localdomain';
4091d7d5
SI
1350 # postfix will not parse a hostname with trailing '.'
1351 $domain =~ s/^(.*)\.$/$1/;
08a1d1d9
SI
1352 $dnsinfo->{domain} = $domain;
1353
1354 $dnsinfo->{fqdn} = "$nodename.$domain";
1355
1356 return $dnsinfo;
1357}
1358
07b3face
DM
1359sub get_template_vars {
1360 my ($self) = @_;
4ccdc564
DM
1361
1362 my $vars = { pmg => $self->get_config() };
1363
08a1d1d9
SI
1364 my $dnsinfo = get_host_dns_info();
1365 $vars->{dns} = $dnsinfo;
1366 my $int_ip = PMG::Cluster::remote_node_ip($dnsinfo->{hostname});
f609bf7f 1367 $vars->{ipconfig}->{int_ip} = $int_ip;
f609bf7f 1368
dc21c233
DC
1369 my $transportnets = {};
1370 my $mynetworks = {
1371 '127.0.0.0/8' => 1,
1372 '[::1]/128', => 1,
1373 };
ba323310 1374
5e37e665 1375 if (my $tmap = PVE::INotify::read_file('transport')) {
dc21c233 1376 foreach my $domain (keys %$tmap) {
5e37e665
DM
1377 my $data = $tmap->{$domain};
1378 my $host = $data->{host};
1379 if ($host =~ m/^$IPV4RE$/) {
dc21c233
DC
1380 $transportnets->{"$host/32"} = 1;
1381 $mynetworks->{"$host/32"} = 1;
8e3b0ef1 1382 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
dc21c233
DC
1383 $transportnets->{"[$1]/128"} = 1;
1384 $mynetworks->{"[$1]/128"} = 1;
5e37e665 1385 }
ba323310
DM
1386 }
1387 }
1388
dc21c233 1389 $vars->{postfix}->{transportnets} = join(' ', sort keys %$transportnets);
1e88a529 1390
c48fefb0
TL
1391 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1392 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
1393 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
dc21c233 1394 $mynetworks->{"[$1]/$2"} = 1;
c48fefb0 1395 } else {
dc21c233 1396 $mynetworks->{$int_net_cidr} = 1;
c48fefb0 1397 }
1e88a529 1398 } else {
c48fefb0 1399 if ($int_ip =~ m/^$IPV6RE$/) {
dc21c233 1400 $mynetworks->{"[$int_ip]/128"} = 1;
c48fefb0 1401 } else {
dc21c233 1402 $mynetworks->{"$int_ip/32"} = 1;
c48fefb0 1403 }
1e88a529
DM
1404 }
1405 }
f609bf7f 1406
bef31f06 1407 my $netlist = PVE::INotify::read_file('mynetworks');
dc21c233 1408 foreach my $cidr (keys %$netlist) {
30c74bd2
CH
1409 my $ip = PVE::Network::IP_from_cidr($cidr);
1410
1411 if (!$ip) {
1412 warn "failed to parse mynetworks entry '$cidr', ignoring\n";
1413 } elsif ($ip->version() == 4) {
1414 $mynetworks->{$ip->prefix()} = 1;
136e6c92 1415 } else {
30c74bd2
CH
1416 my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
1417 $mynetworks->{$address} = 1;
136e6c92
DM
1418 }
1419 }
6d473888 1420
f609bf7f
DM
1421 # add default relay to mynetworks
1422 if (my $relay = $self->get('mail', 'relay')) {
ba323310 1423 if ($relay =~ m/^$IPV4RE$/) {
dc21c233 1424 $mynetworks->{"$relay/32"} = 1;
ba323310 1425 } elsif ($relay =~ m/^$IPV6RE$/) {
dc21c233 1426 $mynetworks->{"[$relay]/128"} = 1;
f609bf7f 1427 } else {
66af5153 1428 # DNS name - do nothing ?
f609bf7f
DM
1429 }
1430 }
1431
dc21c233 1432 $vars->{postfix}->{mynetworks} = join(' ', sort keys %$mynetworks);
f609bf7f 1433
20125a71
DM
1434 # normalize dnsbl_sites
1435 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1436 if (scalar(@dnsbl_sites)) {
1437 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1438 }
1439
11247512
AP
1440 $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
1441
f609bf7f
DM
1442 my $usepolicy = 0;
1443 $usepolicy = 1 if $self->get('mail', 'greylist') ||
aa7c3745 1444 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
f609bf7f
DM
1445 $vars->{postfix}->{usepolicy} = $usepolicy;
1446
c48fefb0
TL
1447 if (!defined($int_ip)) {
1448 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1449 $vars->{postfix}->{int_ip} = '127.0.0.1';
1450 } elsif ($int_ip =~ m/^$IPV6RE$/) {
2664d3cb
DM
1451 $vars->{postfix}->{int_ip} = "[$int_ip]";
1452 } else {
1453 $vars->{postfix}->{int_ip} = $int_ip;
1454 }
1455
08a1d1d9 1456 my $wlbr = $dnsinfo->{fqdn};
ed5fa523
DM
1457 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1458 $wlbr .= " $r"
1459 }
1460 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1461
11081cf6
DM
1462 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1463 eval {
1464 my $uri = URI->new($proxy);
1465 my $host = $uri->host;
1466 my $port = $uri->port // 8080;
1467 if ($host) {
1468 my $data = { host => $host, port => $port };
1469 if (my $ui = $uri->userinfo) {
1470 my ($username, $pw) = split(/:/, $ui, 2);
1471 $data->{username} = $username;
1472 $data->{password} = $pw if defined($pw);
1473 }
1474 $vars->{proxy} = $data;
1475 }
1476 };
1477 warn "parse http_proxy failed - $@" if $@;
1478 }
2005e4b3 1479 $vars->{postgres}->{version} = PMG::Utils::get_pg_server_version();
11081cf6 1480
07b3face
DM
1481 return $vars;
1482}
1483
99e7a8b1
TL
1484# reads the $filename and checks if it's equal as the $cmp string passed
1485my sub file_content_equals_str {
1486 my ($filename, $cmp) = @_;
1487
1488 return if !-f $filename;
1489 my $current = PVE::Tools::file_get_contents($filename, 128*1024);
1490 return defined($current) && $current eq $cmp; # no change
1491}
1492
310daf18
DM
1493# use one global TT cache
1494our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1495
1496my $template_toolkit;
1497
1498sub get_template_toolkit {
1499
1500 return $template_toolkit if $template_toolkit;
1501
1502 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1503
1504 return $template_toolkit;
1505}
1506
07b3face
DM
1507# rewrite file from template
1508# return true if file has changed
1509sub rewrite_config_file {
1510 my ($self, $tmplname, $dstfn) = @_;
1511
1512 my $demo = $self->get('admin', 'demo');
1513
07b3face 1514 if ($demo) {
310daf18
DM
1515 my $demosrc = "$tmplname.demo";
1516 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
07b3face
DM
1517 }
1518
c248d69f 1519 my ($perm, $uid, $gid);
07b3face 1520
d1156caa 1521 if ($dstfn eq '/etc/clamav/freshclam.conf') {
07b3face
DM
1522 # needed if file contains a HTTPProxyPasswort
1523
1524 $uid = getpwnam('clamav');
1525 $gid = getgrnam('adm');
1526 $perm = 0600;
1527 }
1528
310daf18 1529 my $tt = get_template_toolkit();
07b3face
DM
1530
1531 my $vars = $self->get_template_vars();
1532
c248d69f 1533 my $output = '';
07b3face 1534
99e7a8b1 1535 $tt->process($tmplname, $vars, \$output) || die $tt->error() . "\n";
07b3face 1536
99e7a8b1 1537 return 0 if file_content_equals_str($dstfn, $output); # no change -> nothing to do
07b3face
DM
1538
1539 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1540
1541 if (defined($uid) && defined($gid)) {
1542 chown($uid, $gid, $dstfn);
1543 }
1544
1545 return 1;
4ccdc564
DM
1546}
1547
9123cab5
DM
1548# rewrite spam configuration
1549sub rewrite_config_spam {
1550 my ($self) = @_;
1551
1552 my $use_awl = $self->get('spam', 'use_awl');
1553 my $use_bayes = $self->get('spam', 'use_bayes');
1554 my $use_razor = $self->get('spam', 'use_razor');
1555
17424665
DM
1556 my $changes = 0;
1557
9123cab5 1558 # delete AW and bayes databases if those features are disabled
17424665
DM
1559 if (!$use_awl) {
1560 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1561 }
1562
9123cab5 1563 if (!$use_bayes) {
17424665
DM
1564 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1565 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1566 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
9123cab5
DM
1567 }
1568
f9d8c305 1569 # make sure we have the custom SA files (else cluster sync fails)
9123cab5 1570 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
f9d8c305 1571 IO::File->new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
9123cab5 1572
17424665
DM
1573 $changes = 1 if $self->rewrite_config_file(
1574 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1575
1576 $changes = 1 if $self->rewrite_config_file(
1577 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1578
1579 $changes = 1 if $self->rewrite_config_file(
1580 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1581
1582 $changes = 1 if $self->rewrite_config_file(
1583 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
9123cab5 1584
b1b20b37
SI
1585 $changes = 1 if $self->rewrite_config_file(
1586 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
1587
1689ee83
SI
1588 $changes = 1 if $self->rewrite_config_file(
1589 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
1590
9123cab5
DM
1591 if ($use_razor) {
1592 mkdir "/root/.razor";
17424665
DM
1593
1594 $changes = 1 if $self->rewrite_config_file(
1595 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1596
9123cab5
DM
1597 if (! -e '/root/.razor/identity') {
1598 eval {
1599 my $timeout = 30;
17424665
DM
1600 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1601 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
9123cab5
DM
1602 };
1603 my $err = $@;
b902c0b8 1604 syslog('info', "registering razor failed: $err") if $err;
9123cab5
DM
1605 }
1606 }
17424665
DM
1607
1608 return $changes;
9123cab5
DM
1609}
1610
ac5d1312
DM
1611# rewrite ClamAV configuration
1612sub rewrite_config_clam {
1613 my ($self) = @_;
1614
17424665
DM
1615 return $self->rewrite_config_file(
1616 'clamd.conf.in', '/etc/clamav/clamd.conf');
1617}
1618
1619sub rewrite_config_freshclam {
1620 my ($self) = @_;
1621
1622 return $self->rewrite_config_file(
1623 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
ac5d1312
DM
1624}
1625
86737f12
DM
1626sub rewrite_config_postgres {
1627 my ($self) = @_;
1628
2005e4b3
SI
1629 my $pg_maj_version = PMG::Utils::get_pg_server_version();
1630 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
86737f12 1631
17424665
DM
1632 my $changes = 0;
1633
1634 $changes = 1 if $self->rewrite_config_file(
1635 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1636
1637 $changes = 1 if $self->rewrite_config_file(
1638 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1639
1640 return $changes;
86737f12
DM
1641}
1642
1643# rewrite /root/.forward
1644sub rewrite_dot_forward {
1645 my ($self) = @_;
1646
c248d69f 1647 my $dstfn = '/root/.forward';
86737f12 1648
0bb9a01a 1649 my $email = $self->get('admin', 'email');
c248d69f 1650
e14fda7a 1651 my $output = '';
86737f12 1652 if ($email && $email =~ m/\s*(\S+)\s*/) {
c248d69f 1653 $output = "$1\n";
86737f12
DM
1654 } else {
1655 # empty .forward does not forward mails (see man local)
1656 }
99e7a8b1 1657 return 0 if file_content_equals_str($dstfn, $output); # no change -> nothing to do
c248d69f
DM
1658
1659 PVE::Tools::file_set_contents($dstfn, $output);
1660
1661 return 1;
86737f12
DM
1662}
1663
d15630a9
DM
1664my $write_smtp_whitelist = sub {
1665 my ($filename, $data, $action) = @_;
1666
1667 $action = 'OK' if !$action;
1668
d15630a9
DM
1669 my $new = '';
1670 foreach my $k (sort keys %$data) {
1671 $new .= "$k $action\n";
1672 }
99e7a8b1 1673 return 0 if file_content_equals_str($filename, $new); # no change -> nothing to do
d15630a9
DM
1674
1675 PVE::Tools::file_set_contents($filename, $new);
1676
1677 PMG::Utils::run_postmap($filename);
1678
1679 return 1;
1680};
1681
f9967a49 1682sub rewrite_postfix_whitelist {
d15630a9
DM
1683 my ($rulecache) = @_;
1684
1685 # see man page for regexp_table for postfix regex table format
1686
1687 # we use a hash to avoid duplicate entries in regex tables
1688 my $tolist = {};
1689 my $fromlist = {};
1690 my $clientlist = {};
1691
1692 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1693 my $oclass = ref($obj);
1694 if ($oclass eq 'PMG::RuleDB::Receiver') {
1695 my $addr = PMG::Utils::quote_regex($obj->{address});
1696 $tolist->{"/^$addr\$/"} = 1;
1697 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1698 my $addr = PMG::Utils::quote_regex($obj->{address});
1699 $tolist->{"/^.+\@$addr\$/"} = 1;
1700 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1701 my $addr = $obj->{address};
1702 $addr =~ s|/|\\/|g;
1703 $tolist->{"/^$addr\$/"} = 1;
1704 }
1705 }
1706
1707 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1708 my $oclass = ref($obj);
1709 my $addr = PMG::Utils::quote_regex($obj->{address});
1710 if ($oclass eq 'PMG::RuleDB::EMail') {
1711 my $addr = PMG::Utils::quote_regex($obj->{address});
1712 $fromlist->{"/^$addr\$/"} = 1;
1713 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1714 my $addr = PMG::Utils::quote_regex($obj->{address});
1715 $fromlist->{"/^.+\@$addr\$/"} = 1;
1716 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1717 my $addr = $obj->{address};
1718 $addr =~ s|/|\\/|g;
1719 $fromlist->{"/^$addr\$/"} = 1;
1720 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1721 $clientlist->{$obj->{address}} = 1;
1722 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1723 $clientlist->{$obj->{address}} = 1;
1724 }
1725 }
1726
1727 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1728 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1729 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1730 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1731};
1732
f609bf7f
DM
1733# rewrite /etc/postfix/*
1734sub rewrite_config_postfix {
d15630a9 1735 my ($self, $rulecache) = @_;
f609bf7f 1736
3546daf0 1737 # make sure we have required files (else postfix start fails)
3546daf0 1738 IO::File->new($transport_map_filename, 'a', 0644);
f609bf7f 1739
17424665
DM
1740 my $changes = 0;
1741
f609bf7f
DM
1742 if ($self->get('mail', 'tls')) {
1743 eval {
bc44eb02 1744 PMG::Utils::gen_proxmox_tls_cert();
f609bf7f 1745 };
b902c0b8 1746 syslog ('info', "generating certificate failed: $@") if $@;
f609bf7f
DM
1747 }
1748
17424665
DM
1749 $changes = 1 if $self->rewrite_config_file(
1750 'main.cf.in', '/etc/postfix/main.cf');
1751
1752 $changes = 1 if $self->rewrite_config_file(
1753 'master.cf.in', '/etc/postfix/master.cf');
1754
a0d4ce8d
DM
1755 # make sure we have required files (else postfix start fails)
1756 # Note: postmap need a valid /etc/postfix/main.cf configuration
1757 postmap_pmg_domains();
1758 postmap_pmg_transport();
1759 postmap_tls_policy();
fbb8db63 1760 postmap_tls_inbound_domains();
a0d4ce8d 1761
f9967a49 1762 rewrite_postfix_whitelist($rulecache) if $rulecache;
d15630a9 1763
f609bf7f
DM
1764 # make sure aliases.db is up to date
1765 system('/usr/bin/newaliases');
17424665
DM
1766
1767 return $changes;
f609bf7f
DM
1768}
1769
e5b31a61
SI
1770#parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1771my $pmg_service_params = {
71c68eba
SI
1772 mail => {
1773 hide_received => 1,
1774 ndr_on_block => 1,
ceb383cb 1775 smtputf8 => 1,
71c68eba 1776 },
e4b38221
SI
1777 admin => {
1778 dkim_selector => 1,
1779 dkim_sign => 1,
1780 dkim_sign_all_mail => 1,
1781 },
e5b31a61
SI
1782};
1783
1784my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1785my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1786
1787sub dump_smtp_filter_config {
1788 my ($self) = @_;
1789
1790 my $conf = '';
1791 my $val;
1792 foreach my $sec (sort keys %$pmg_service_params) {
1793 my $conf_sec = $self->{ids}->{$sec} // {};
1794 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1795 $val = $conf_sec->{$key};
1796 $conf .= "$sec.$key:$val\n" if defined($val);
1797 }
1798 }
1799
1800 return $conf;
1801}
1802
1803sub compare_smtp_filter_config {
1804 my ($self) = @_;
1805
1806 my $ret = 0;
1807 my $old;
1808 eval {
1809 $old = PVE::Tools::file_get_contents($smtp_filter_cfg);
1810 };
1811
1812 if (my $err = $@) {
1813 syslog ('warning', "reloading pmg-smtp-filter: $err");
1814 $ret = 1;
1815 } else {
1816 my $new = $self->dump_smtp_filter_config();
1817 $ret = 1 if $old ne $new;
1818 }
1819
1820 $self->write_smtp_filter_config() if $ret;
1821
1822 return $ret;
1823}
1824
1825# writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1826# on config change
1827sub write_smtp_filter_config {
1828 my ($self) = @_;
1829
1830 PVE::Tools::lock_file($smtp_filter_cfg_lock, undef, sub {
1831 PVE::Tools::file_set_contents($smtp_filter_cfg,
1832 $self->dump_smtp_filter_config());
1833 });
1834
1835 die $@ if $@;
1836}
1837
f983300f 1838sub rewrite_config {
d15630a9 1839 my ($self, $rulecache, $restart_services, $force_restart) = @_;
c248d69f 1840
798df412
DM
1841 $force_restart = {} if ! $force_restart;
1842
e5bd6522
TL
1843 my $log_restart = sub {
1844 syslog ('info', "configuration change detected for '$_[0]', restarting");
1845 };
1846
d15630a9 1847 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
798df412 1848 $force_restart->{postfix}) {
e5bd6522 1849 $log_restart->('postfix');
2473cb81 1850 PMG::Utils::service_cmd('postfix', 'reload');
c248d69f
DM
1851 }
1852
1853 if ($self->rewrite_dot_forward() && $restart_services) {
1854 # no need to restart anything
1855 }
1856
1857 if ($self->rewrite_config_postgres() && $restart_services) {
1858 # do nothing (too many side effects)?
1859 # does not happen anyways, because config does not change.
1860 }
f983300f 1861
798df412
DM
1862 if (($self->rewrite_config_spam() && $restart_services) ||
1863 $force_restart->{spam}) {
e5bd6522 1864 $log_restart->('pmg-smtp-filter');
c248d69f
DM
1865 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1866 }
1867
798df412
DM
1868 if (($self->rewrite_config_clam() && $restart_services) ||
1869 $force_restart->{clam}) {
e5bd6522 1870 $log_restart->('clamav-daemon');
8f87fe74 1871 PMG::Utils::service_cmd('clamav-daemon', 'restart');
c248d69f
DM
1872 }
1873
798df412
DM
1874 if (($self->rewrite_config_freshclam() && $restart_services) ||
1875 $force_restart->{freshclam}) {
e5bd6522 1876 $log_restart->('clamav-freshclam');
8f87fe74 1877 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
c248d69f 1878 }
e5b31a61
SI
1879
1880 if (($self->compare_smtp_filter_config() && $restart_services) ||
1881 $force_restart->{spam}) {
1882 syslog ('info', "scheduled reload for pmg-smtp-filter");
1883 PMG::Utils::reload_smtp_filter();
1884 }
f983300f
DM
1885}
1886
7e0e6dbe 18871;