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