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