]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/Config.pm
config write_transport_map: code cleanup
[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
1b449731
SI
1036sub read_tls_policy {
1037 my ($filename, $fh) = @_;
1038
1039 return {} if !defined($fh);
1040
1041 my $tls_policy = {};
1042
1043 while (defined(my $line = <$fh>)) {
1044 chomp $line;
1045 next if $line =~ m/^\s*$/;
1046 next if $line =~ m/^#(.*)\s*$/;
1047
1048 my $parse_error = sub {
1049 my ($err) = @_;
dfbfa155 1050 die "parse error in '$filename': $line - $err";
1b449731
SI
1051 };
1052
1053 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1054 my ($domain, $policy) = ($1, $2);
1055
1056 eval {
1057 pmg_verify_transport_domain($domain);
1058 pmg_verify_tls_policy($policy);
1059 };
1060 if (my $err = $@) {
1061 $parse_error->($err);
1062 next;
1063 }
1064
1065 $tls_policy->{$domain} = {
1066 domain => $domain,
1067 policy => $policy,
1068 };
1069 } else {
1070 $parse_error->('wrong format');
1071 }
1072 }
1073
1074 return $tls_policy;
1075}
1076
1077sub write_tls_policy {
1078 my ($filename, $fh, $tls_policy) = @_;
1079
1080 return if !$tls_policy;
1081
1082 foreach my $domain (sort keys %$tls_policy) {
1083 my $entry = $tls_policy->{$domain};
1084 PVE::Tools::safe_print(
1085 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1086 }
1087}
1088
959aaeba 1089my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1b449731
SI
1090PVE::INotify::register_file('tls_policy', $tls_policy_map_filename,
1091 \&read_tls_policy,
1092 \&write_tls_policy,
1093 undef, always_call_parser => 1);
959aaeba
DM
1094
1095sub postmap_tls_policy {
1096 PMG::Utils::run_postmap($tls_policy_map_filename);
1097}
1098
cd533938 1099my $transport_map_filename = "/etc/pmg/transport";
3546daf0 1100
3118b703
DM
1101sub postmap_pmg_transport {
1102 PMG::Utils::run_postmap($transport_map_filename);
1103}
1104
3546daf0
DM
1105sub read_transport_map {
1106 my ($filename, $fh) = @_;
1107
1108 return [] if !defined($fh);
1109
1110 my $res = {};
1111
3118b703 1112 my $comment = '';
b7c49fec 1113
3546daf0
DM
1114 while (defined(my $line = <$fh>)) {
1115 chomp $line;
1116 next if $line =~ m/^\s*$/;
3118b703
DM
1117 if ($line =~ m/^#(.*)\s*$/) {
1118 $comment = $1;
1119 next;
1120 }
3546daf0 1121
b7c49fec
DM
1122 my $parse_error = sub {
1123 my ($err) = @_;
1124 warn "parse error in '$filename': $line - $err";
1125 $comment = '';
1126 };
1127
10d97956
JZ
1128 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1129 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
3546daf0 1130
22c25daf 1131 eval { pmg_verify_transport_domain_or_email($domain); };
b7c49fec
DM
1132 if (my $err = $@) {
1133 $parse_error->($err);
1134 next;
1135 }
53904163 1136 my $use_mx = 1;
3546daf0
DM
1137 if ($host =~ m/^\[(.*)\]$/) {
1138 $host = $1;
53904163 1139 $use_mx = 0;
3546daf0 1140 }
0280ecd4 1141 $use_mx = 0 if ($protocol eq "lmtp");
3546daf0 1142
b7c49fec
DM
1143 eval { PVE::JSONSchema::pve_verify_address($host); };
1144 if (my $err = $@) {
1145 $parse_error->($err);
1146 next;
1147 }
1148
3118b703
DM
1149 my $data = {
1150 domain => $domain,
10d97956 1151 protocol => $protocol,
3118b703
DM
1152 host => $host,
1153 port => $port,
53904163 1154 use_mx => $use_mx,
3118b703
DM
1155 comment => $comment,
1156 };
1157 $res->{$domain} = $data;
1158 $comment = '';
1159 } else {
b7c49fec 1160 $parse_error->('wrong format');
3546daf0
DM
1161 }
1162 }
1163
3118b703 1164 return $res;
3546daf0
DM
1165}
1166
cd533938 1167sub write_transport_map {
3546daf0
DM
1168 my ($filename, $fh, $tmap) = @_;
1169
1170 return if !$tmap;
1171
3118b703
DM
1172 foreach my $domain (sort keys %$tmap) {
1173 my $data = $tmap->{$domain};
3546daf0 1174
3118b703
DM
1175 my $comment = $data->{comment};
1176 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1177 if defined($comment) && $comment !~ m/^\s*$/;
1178
256ff1ba 1179 my $bracket_host = !$data->{use_mx};
ba323310 1180
204fd68b 1181 if ($data->{protocol} eq 'lmtp') {
0280ecd4 1182 $bracket_host = 0;
204fd68b 1183 $data->{protocol} .= ":inet";
10d97956 1184 }
0280ecd4 1185 $bracket_host = 1 if $data->{host} =~ m/^(?:$IPV4RE|$IPV6RE)$/i;
256ff1ba 1186 my $host = $bracket_host ? "[$data->{host}]" : $data->{host};
0280ecd4 1187
256ff1ba 1188 PVE::Tools::safe_print($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
3546daf0
DM
1189 }
1190}
1191
1192PVE::INotify::register_file('transport', $transport_map_filename,
1193 \&read_transport_map,
cd533938 1194 \&write_transport_map,
3546daf0 1195 undef, always_call_parser => 1);
7e0e6dbe 1196
4ccdc564
DM
1197# config file generation using templates
1198
08a1d1d9
SI
1199sub get_host_dns_info {
1200 my ($self) = @_;
1201
1202 my $dnsinfo = {};
1203 my $nodename = PVE::INotify::nodename();
1204
1205 $dnsinfo->{hostname} = $nodename;
1206 my $resolv = PVE::INotify::read_file('resolvconf');
1207
1208 my $domain = $resolv->{search} // 'localdomain';
1209 $dnsinfo->{domain} = $domain;
1210
1211 $dnsinfo->{fqdn} = "$nodename.$domain";
1212
1213 return $dnsinfo;
1214}
1215
07b3face
DM
1216sub get_template_vars {
1217 my ($self) = @_;
4ccdc564
DM
1218
1219 my $vars = { pmg => $self->get_config() };
1220
08a1d1d9
SI
1221 my $dnsinfo = get_host_dns_info();
1222 $vars->{dns} = $dnsinfo;
1223 my $int_ip = PMG::Cluster::remote_node_ip($dnsinfo->{hostname});
f609bf7f 1224 $vars->{ipconfig}->{int_ip} = $int_ip;
f609bf7f 1225
ba323310
DM
1226 my $transportnets = [];
1227
5e37e665
DM
1228 if (my $tmap = PVE::INotify::read_file('transport')) {
1229 foreach my $domain (sort keys %$tmap) {
1230 my $data = $tmap->{$domain};
1231 my $host = $data->{host};
1232 if ($host =~ m/^$IPV4RE$/) {
1233 push @$transportnets, "$host/32";
1234 } elsif ($host =~ m/^$IPV6RE$/) {
1235 push @$transportnets, "[$host]/128";
1236 }
ba323310
DM
1237 }
1238 }
1239
f609bf7f
DM
1240 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
1241
1242 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1e88a529
DM
1243
1244 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
136e6c92
DM
1245 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1246 push @$mynetworks, "[$1]/$2";
1247 } else {
1248 push @$mynetworks, $int_net_cidr;
1249 }
1e88a529
DM
1250 } else {
1251 if ($int_ip =~ m/^$IPV6RE$/) {
136e6c92 1252 push @$mynetworks, "[$int_ip]/128";
1e88a529
DM
1253 } else {
1254 push @$mynetworks, "$int_ip/32";
1255 }
1256 }
f609bf7f 1257
bef31f06 1258 my $netlist = PVE::INotify::read_file('mynetworks');
b8733ec1 1259 foreach my $cidr (sort keys %$netlist) {
136e6c92
DM
1260 if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1261 push @$mynetworks, "[$1]/$2";
1262 } else {
1263 push @$mynetworks, $cidr;
1264 }
1265 }
6d473888
DM
1266
1267 push @$mynetworks, @$transportnets;
1268
f609bf7f
DM
1269 # add default relay to mynetworks
1270 if (my $relay = $self->get('mail', 'relay')) {
ba323310 1271 if ($relay =~ m/^$IPV4RE$/) {
f609bf7f 1272 push @$mynetworks, "$relay/32";
ba323310 1273 } elsif ($relay =~ m/^$IPV6RE$/) {
f609bf7f
DM
1274 push @$mynetworks, "[$relay]/128";
1275 } else {
66af5153 1276 # DNS name - do nothing ?
f609bf7f
DM
1277 }
1278 }
1279
1280 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1281
20125a71
DM
1282 # normalize dnsbl_sites
1283 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1284 if (scalar(@dnsbl_sites)) {
1285 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1286 }
1287
11247512
AP
1288 $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
1289
f609bf7f
DM
1290 my $usepolicy = 0;
1291 $usepolicy = 1 if $self->get('mail', 'greylist') ||
0eae95d3 1292 $self->get('mail', 'spf');
f609bf7f
DM
1293 $vars->{postfix}->{usepolicy} = $usepolicy;
1294
2664d3cb
DM
1295 if ($int_ip =~ m/^$IPV6RE$/) {
1296 $vars->{postfix}->{int_ip} = "[$int_ip]";
1297 } else {
1298 $vars->{postfix}->{int_ip} = $int_ip;
1299 }
1300
08a1d1d9 1301 my $wlbr = $dnsinfo->{fqdn};
ed5fa523
DM
1302 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1303 $wlbr .= " $r"
1304 }
1305 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1306
11081cf6
DM
1307 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1308 eval {
1309 my $uri = URI->new($proxy);
1310 my $host = $uri->host;
1311 my $port = $uri->port // 8080;
1312 if ($host) {
1313 my $data = { host => $host, port => $port };
1314 if (my $ui = $uri->userinfo) {
1315 my ($username, $pw) = split(/:/, $ui, 2);
1316 $data->{username} = $username;
1317 $data->{password} = $pw if defined($pw);
1318 }
1319 $vars->{proxy} = $data;
1320 }
1321 };
1322 warn "parse http_proxy failed - $@" if $@;
1323 }
2005e4b3 1324 $vars->{postgres}->{version} = PMG::Utils::get_pg_server_version();
11081cf6 1325
07b3face
DM
1326 return $vars;
1327}
1328
310daf18
DM
1329# use one global TT cache
1330our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1331
1332my $template_toolkit;
1333
1334sub get_template_toolkit {
1335
1336 return $template_toolkit if $template_toolkit;
1337
1338 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1339
1340 return $template_toolkit;
1341}
1342
07b3face
DM
1343# rewrite file from template
1344# return true if file has changed
1345sub rewrite_config_file {
1346 my ($self, $tmplname, $dstfn) = @_;
1347
1348 my $demo = $self->get('admin', 'demo');
1349
07b3face 1350 if ($demo) {
310daf18
DM
1351 my $demosrc = "$tmplname.demo";
1352 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
07b3face
DM
1353 }
1354
c248d69f 1355 my ($perm, $uid, $gid);
07b3face 1356
d1156caa 1357 if ($dstfn eq '/etc/clamav/freshclam.conf') {
07b3face
DM
1358 # needed if file contains a HTTPProxyPasswort
1359
1360 $uid = getpwnam('clamav');
1361 $gid = getgrnam('adm');
1362 $perm = 0600;
1363 }
1364
310daf18 1365 my $tt = get_template_toolkit();
07b3face
DM
1366
1367 my $vars = $self->get_template_vars();
1368
c248d69f 1369 my $output = '';
07b3face 1370
310daf18 1371 $tt->process($tmplname, $vars, \$output) ||
60f82a46 1372 die $tt->error() . "\n";
07b3face
DM
1373
1374 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1375
1376 return 0 if defined($old) && ($old eq $output); # no change
1377
1378 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1379
1380 if (defined($uid) && defined($gid)) {
1381 chown($uid, $gid, $dstfn);
1382 }
1383
1384 return 1;
4ccdc564
DM
1385}
1386
9123cab5
DM
1387# rewrite spam configuration
1388sub rewrite_config_spam {
1389 my ($self) = @_;
1390
1391 my $use_awl = $self->get('spam', 'use_awl');
1392 my $use_bayes = $self->get('spam', 'use_bayes');
1393 my $use_razor = $self->get('spam', 'use_razor');
1394
17424665
DM
1395 my $changes = 0;
1396
9123cab5 1397 # delete AW and bayes databases if those features are disabled
17424665
DM
1398 if (!$use_awl) {
1399 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1400 }
1401
9123cab5 1402 if (!$use_bayes) {
17424665
DM
1403 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1404 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1405 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
9123cab5
DM
1406 }
1407
f9d8c305 1408 # make sure we have the custom SA files (else cluster sync fails)
9123cab5 1409 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
f9d8c305 1410 IO::File->new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
9123cab5 1411
17424665
DM
1412 $changes = 1 if $self->rewrite_config_file(
1413 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1414
1415 $changes = 1 if $self->rewrite_config_file(
1416 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1417
1418 $changes = 1 if $self->rewrite_config_file(
1419 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1420
1421 $changes = 1 if $self->rewrite_config_file(
1422 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
9123cab5
DM
1423
1424 if ($use_razor) {
1425 mkdir "/root/.razor";
17424665
DM
1426
1427 $changes = 1 if $self->rewrite_config_file(
1428 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1429
9123cab5
DM
1430 if (! -e '/root/.razor/identity') {
1431 eval {
1432 my $timeout = 30;
17424665
DM
1433 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1434 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
9123cab5
DM
1435 };
1436 my $err = $@;
b902c0b8 1437 syslog('info', "registering razor failed: $err") if $err;
9123cab5
DM
1438 }
1439 }
17424665
DM
1440
1441 return $changes;
9123cab5
DM
1442}
1443
ac5d1312
DM
1444# rewrite ClamAV configuration
1445sub rewrite_config_clam {
1446 my ($self) = @_;
1447
17424665
DM
1448 return $self->rewrite_config_file(
1449 'clamd.conf.in', '/etc/clamav/clamd.conf');
1450}
1451
1452sub rewrite_config_freshclam {
1453 my ($self) = @_;
1454
1455 return $self->rewrite_config_file(
1456 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
ac5d1312
DM
1457}
1458
86737f12
DM
1459sub rewrite_config_postgres {
1460 my ($self) = @_;
1461
2005e4b3
SI
1462 my $pg_maj_version = PMG::Utils::get_pg_server_version();
1463 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
86737f12 1464
17424665
DM
1465 my $changes = 0;
1466
1467 $changes = 1 if $self->rewrite_config_file(
1468 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1469
1470 $changes = 1 if $self->rewrite_config_file(
1471 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1472
1473 return $changes;
86737f12
DM
1474}
1475
1476# rewrite /root/.forward
1477sub rewrite_dot_forward {
1478 my ($self) = @_;
1479
c248d69f 1480 my $dstfn = '/root/.forward';
86737f12 1481
0bb9a01a 1482 my $email = $self->get('admin', 'email');
c248d69f 1483
e14fda7a 1484 my $output = '';
86737f12 1485 if ($email && $email =~ m/\s*(\S+)\s*/) {
c248d69f 1486 $output = "$1\n";
86737f12
DM
1487 } else {
1488 # empty .forward does not forward mails (see man local)
1489 }
17424665 1490
c248d69f
DM
1491 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1492
1493 return 0 if defined($old) && ($old eq $output); # no change
1494
1495 PVE::Tools::file_set_contents($dstfn, $output);
1496
1497 return 1;
86737f12
DM
1498}
1499
d15630a9
DM
1500my $write_smtp_whitelist = sub {
1501 my ($filename, $data, $action) = @_;
1502
1503 $action = 'OK' if !$action;
1504
1505 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1506 if -f $filename;
1507
1508 my $new = '';
1509 foreach my $k (sort keys %$data) {
1510 $new .= "$k $action\n";
1511 }
1512
1513 return 0 if defined($old) && ($old eq $new); # no change
1514
1515 PVE::Tools::file_set_contents($filename, $new);
1516
1517 PMG::Utils::run_postmap($filename);
1518
1519 return 1;
1520};
1521
f9967a49 1522sub rewrite_postfix_whitelist {
d15630a9
DM
1523 my ($rulecache) = @_;
1524
1525 # see man page for regexp_table for postfix regex table format
1526
1527 # we use a hash to avoid duplicate entries in regex tables
1528 my $tolist = {};
1529 my $fromlist = {};
1530 my $clientlist = {};
1531
1532 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1533 my $oclass = ref($obj);
1534 if ($oclass eq 'PMG::RuleDB::Receiver') {
1535 my $addr = PMG::Utils::quote_regex($obj->{address});
1536 $tolist->{"/^$addr\$/"} = 1;
1537 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1538 my $addr = PMG::Utils::quote_regex($obj->{address});
1539 $tolist->{"/^.+\@$addr\$/"} = 1;
1540 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1541 my $addr = $obj->{address};
1542 $addr =~ s|/|\\/|g;
1543 $tolist->{"/^$addr\$/"} = 1;
1544 }
1545 }
1546
1547 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1548 my $oclass = ref($obj);
1549 my $addr = PMG::Utils::quote_regex($obj->{address});
1550 if ($oclass eq 'PMG::RuleDB::EMail') {
1551 my $addr = PMG::Utils::quote_regex($obj->{address});
1552 $fromlist->{"/^$addr\$/"} = 1;
1553 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1554 my $addr = PMG::Utils::quote_regex($obj->{address});
1555 $fromlist->{"/^.+\@$addr\$/"} = 1;
1556 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1557 my $addr = $obj->{address};
1558 $addr =~ s|/|\\/|g;
1559 $fromlist->{"/^$addr\$/"} = 1;
1560 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1561 $clientlist->{$obj->{address}} = 1;
1562 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1563 $clientlist->{$obj->{address}} = 1;
1564 }
1565 }
1566
1567 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1568 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1569 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1570 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1571};
1572
f609bf7f
DM
1573# rewrite /etc/postfix/*
1574sub rewrite_config_postfix {
d15630a9 1575 my ($self, $rulecache) = @_;
f609bf7f 1576
3546daf0 1577 # make sure we have required files (else postfix start fails)
3546daf0 1578 IO::File->new($transport_map_filename, 'a', 0644);
f609bf7f 1579
17424665
DM
1580 my $changes = 0;
1581
f609bf7f
DM
1582 if ($self->get('mail', 'tls')) {
1583 eval {
bc44eb02 1584 PMG::Utils::gen_proxmox_tls_cert();
f609bf7f 1585 };
b902c0b8 1586 syslog ('info', "generating certificate failed: $@") if $@;
f609bf7f
DM
1587 }
1588
17424665
DM
1589 $changes = 1 if $self->rewrite_config_file(
1590 'main.cf.in', '/etc/postfix/main.cf');
1591
1592 $changes = 1 if $self->rewrite_config_file(
1593 'master.cf.in', '/etc/postfix/master.cf');
1594
a0d4ce8d
DM
1595 # make sure we have required files (else postfix start fails)
1596 # Note: postmap need a valid /etc/postfix/main.cf configuration
1597 postmap_pmg_domains();
1598 postmap_pmg_transport();
1599 postmap_tls_policy();
1600
f9967a49 1601 rewrite_postfix_whitelist($rulecache) if $rulecache;
d15630a9 1602
f609bf7f
DM
1603 # make sure aliases.db is up to date
1604 system('/usr/bin/newaliases');
17424665
DM
1605
1606 return $changes;
f609bf7f
DM
1607}
1608
e5b31a61
SI
1609#parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1610my $pmg_service_params = {
71c68eba
SI
1611 mail => {
1612 hide_received => 1,
1613 ndr_on_block => 1,
1614 },
e4b38221
SI
1615 admin => {
1616 dkim_selector => 1,
1617 dkim_sign => 1,
1618 dkim_sign_all_mail => 1,
1619 },
e5b31a61
SI
1620};
1621
1622my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1623my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1624
1625sub dump_smtp_filter_config {
1626 my ($self) = @_;
1627
1628 my $conf = '';
1629 my $val;
1630 foreach my $sec (sort keys %$pmg_service_params) {
1631 my $conf_sec = $self->{ids}->{$sec} // {};
1632 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1633 $val = $conf_sec->{$key};
1634 $conf .= "$sec.$key:$val\n" if defined($val);
1635 }
1636 }
1637
1638 return $conf;
1639}
1640
1641sub compare_smtp_filter_config {
1642 my ($self) = @_;
1643
1644 my $ret = 0;
1645 my $old;
1646 eval {
1647 $old = PVE::Tools::file_get_contents($smtp_filter_cfg);
1648 };
1649
1650 if (my $err = $@) {
1651 syslog ('warning', "reloading pmg-smtp-filter: $err");
1652 $ret = 1;
1653 } else {
1654 my $new = $self->dump_smtp_filter_config();
1655 $ret = 1 if $old ne $new;
1656 }
1657
1658 $self->write_smtp_filter_config() if $ret;
1659
1660 return $ret;
1661}
1662
1663# writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1664# on config change
1665sub write_smtp_filter_config {
1666 my ($self) = @_;
1667
1668 PVE::Tools::lock_file($smtp_filter_cfg_lock, undef, sub {
1669 PVE::Tools::file_set_contents($smtp_filter_cfg,
1670 $self->dump_smtp_filter_config());
1671 });
1672
1673 die $@ if $@;
1674}
1675
f983300f 1676sub rewrite_config {
d15630a9 1677 my ($self, $rulecache, $restart_services, $force_restart) = @_;
c248d69f 1678
798df412
DM
1679 $force_restart = {} if ! $force_restart;
1680
e5bd6522
TL
1681 my $log_restart = sub {
1682 syslog ('info', "configuration change detected for '$_[0]', restarting");
1683 };
1684
d15630a9 1685 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
798df412 1686 $force_restart->{postfix}) {
e5bd6522 1687 $log_restart->('postfix');
2473cb81 1688 PMG::Utils::service_cmd('postfix', 'reload');
c248d69f
DM
1689 }
1690
1691 if ($self->rewrite_dot_forward() && $restart_services) {
1692 # no need to restart anything
1693 }
1694
1695 if ($self->rewrite_config_postgres() && $restart_services) {
1696 # do nothing (too many side effects)?
1697 # does not happen anyways, because config does not change.
1698 }
f983300f 1699
798df412
DM
1700 if (($self->rewrite_config_spam() && $restart_services) ||
1701 $force_restart->{spam}) {
e5bd6522 1702 $log_restart->('pmg-smtp-filter');
c248d69f
DM
1703 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1704 }
1705
798df412
DM
1706 if (($self->rewrite_config_clam() && $restart_services) ||
1707 $force_restart->{clam}) {
e5bd6522 1708 $log_restart->('clamav-daemon');
8f87fe74 1709 PMG::Utils::service_cmd('clamav-daemon', 'restart');
c248d69f
DM
1710 }
1711
798df412
DM
1712 if (($self->rewrite_config_freshclam() && $restart_services) ||
1713 $force_restart->{freshclam}) {
e5bd6522 1714 $log_restart->('clamav-freshclam');
8f87fe74 1715 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
c248d69f 1716 }
e5b31a61
SI
1717
1718 if (($self->compare_smtp_filter_config() && $restart_services) ||
1719 $force_restart->{spam}) {
1720 syslog ('info', "scheduled reload for pmg-smtp-filter");
1721 PMG::Utils::reload_smtp_filter();
1722 }
f983300f
DM
1723}
1724
7e0e6dbe 17251;