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