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