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