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