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