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