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