]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/Config.pm
api: nodeconfig: validate acme config before writing
[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',
400 default => 0
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
9dfe7c16 919 my $raw = <$fh> if defined($fh);
7e0e6dbe
DM
920
921 return PMG::Config::Base->parse_config($filename, $raw);
922}
923
924sub write_pmg_conf {
925 my ($filename, $fh, $cfg) = @_;
926
927 my $raw = PMG::Config::Base->write_config($filename, $cfg);
928
929 PVE::Tools::safe_print($filename, $fh, $raw);
930}
931
3278b571 932PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
f62194b2 933 \&read_pmg_conf,
9dfe7c16
DM
934 \&write_pmg_conf,
935 undef, always_call_parser => 1);
7e0e6dbe 936
f609bf7f
DM
937# parsers/writers for other files
938
3278b571 939my $domainsfilename = "/etc/pmg/domains";
f609bf7f 940
c3f4336c
DM
941sub postmap_pmg_domains {
942 PMG::Utils::run_postmap($domainsfilename);
943}
944
f609bf7f
DM
945sub read_pmg_domains {
946 my ($filename, $fh) = @_;
947
b7298186 948 my $domains = {};
f609bf7f 949
b7298186 950 my $comment = '';
f609bf7f
DM
951 if (defined($fh)) {
952 while (defined(my $line = <$fh>)) {
3118b703
DM
953 chomp $line;
954 next if $line =~ m/^\s*$/;
b7298186
DM
955 if ($line =~ m/^#(.*)\s*$/) {
956 $comment = $1;
957 next;
958 }
959 if ($line =~ m/^(\S+)\s.*$/) {
f609bf7f 960 my $domain = $1;
b7298186
DM
961 $domains->{$domain} = {
962 domain => $domain, comment => $comment };
963 $comment = '';
3118b703
DM
964 } else {
965 warn "parse error in '$filename': $line\n";
966 $comment = '';
f609bf7f
DM
967 }
968 }
969 }
970
971 return $domains;
972}
973
974sub write_pmg_domains {
b7298186
DM
975 my ($filename, $fh, $domains) = @_;
976
977 foreach my $domain (sort keys %$domains) {
978 my $comment = $domains->{$domain}->{comment};
979 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
980 if defined($comment) && $comment !~ m/^\s*$/;
f609bf7f 981
6b31da64 982 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
f609bf7f
DM
983 }
984}
985
986PVE::INotify::register_file('domains', $domainsfilename,
987 \&read_pmg_domains,
988 \&write_pmg_domains,
989 undef, always_call_parser => 1);
990
e4b38221
SI
991my $dkimdomainsfile = '/etc/pmg/dkim/domains';
992
993PVE::INotify::register_file('dkimdomains', $dkimdomainsfile,
994 \&read_pmg_domains,
995 \&write_pmg_domains,
996 undef, always_call_parser => 1);
997
bef31f06
DM
998my $mynetworks_filename = "/etc/pmg/mynetworks";
999
bef31f06
DM
1000sub read_pmg_mynetworks {
1001 my ($filename, $fh) = @_;
1002
1003 my $mynetworks = {};
1004
1005 my $comment = '';
1006 if (defined($fh)) {
1007 while (defined(my $line = <$fh>)) {
1008 chomp $line;
1009 next if $line =~ m/^\s*$/;
1010 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1011 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1012 my $cidr = "$network/${prefix_size}";
1013 $mynetworks->{$cidr} = {
1014 cidr => $cidr,
1015 network_address => $network,
1016 prefix_size => $prefix_size,
1017 comment => $comment // '',
1018 };
1019 } else {
1020 warn "parse error in '$filename': $line\n";
1021 }
1022 }
1023 }
1024
1025 return $mynetworks;
1026}
1027
1028sub write_pmg_mynetworks {
1029 my ($filename, $fh, $mynetworks) = @_;
1030
1031 foreach my $cidr (sort keys %$mynetworks) {
1032 my $data = $mynetworks->{$cidr};
1033 my $comment = $data->{comment} // '*';
1034 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
1035 }
1036}
1037
1038PVE::INotify::register_file('mynetworks', $mynetworks_filename,
1039 \&read_pmg_mynetworks,
1040 \&write_pmg_mynetworks,
1041 undef, always_call_parser => 1);
1042
1b449731
SI
1043PVE::JSONSchema::register_format(
1044 'tls-policy', \&pmg_verify_tls_policy);
1045
550f4c47
SI
1046# TODO: extend to parse attributes of the policy
1047my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1b449731
SI
1048sub pmg_verify_tls_policy {
1049 my ($policy, $noerr) = @_;
1050
550f4c47 1051 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1b449731
SI
1052 return undef if $noerr;
1053 die "value '$policy' does not look like a valid tls policy\n";
1054 }
1055 return $policy;
1056}
1057
f1a44c5c
DM
1058PVE::JSONSchema::register_format(
1059 'tls-policy-strict', \&pmg_verify_tls_policy_strict);
550f4c47 1060
f1a44c5c
DM
1061sub pmg_verify_tls_policy_strict {
1062 my ($policy, $noerr) = @_;
550f4c47 1063
f1a44c5c
DM
1064 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1065 return undef if $noerr;
1066 die "value '$policy' does not look like a valid tls policy\n";
1067 }
1068 return $policy;
550f4c47
SI
1069}
1070
644487e3
SI
1071PVE::JSONSchema::register_format(
1072 'transport-domain-or-nexthop', \&pmg_verify_transport_domain_or_nexthop);
1073
1074sub pmg_verify_transport_domain_or_nexthop {
1075 my ($name, $noerr) = @_;
1076
1077 if (pmg_verify_transport_domain($name, 1)) {
1078 return $name;
1079 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1080 my $nexthop = $1;
1081 if ($nexthop =~ m/^\[(.*)\]$/) {
1082 $nexthop = $1;
1083 }
1084 return $name if pmg_verify_transport_address($nexthop, 1);
1085 } else {
1086 return undef if $noerr;
1087 die "value does not look like a valid domain or next-hop\n";
1088 }
1089}
1090
1b449731
SI
1091sub read_tls_policy {
1092 my ($filename, $fh) = @_;
1093
1094 return {} if !defined($fh);
1095
1096 my $tls_policy = {};
1097
1098 while (defined(my $line = <$fh>)) {
1099 chomp $line;
1100 next if $line =~ m/^\s*$/;
1101 next if $line =~ m/^#(.*)\s*$/;
1102
1103 my $parse_error = sub {
1104 my ($err) = @_;
dfbfa155 1105 die "parse error in '$filename': $line - $err";
1b449731
SI
1106 };
1107
1108 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
cce8e372 1109 my ($destination, $policy) = ($1, $2);
1b449731
SI
1110
1111 eval {
cce8e372 1112 pmg_verify_transport_domain_or_nexthop($destination);
1b449731
SI
1113 pmg_verify_tls_policy($policy);
1114 };
1115 if (my $err = $@) {
1116 $parse_error->($err);
1117 next;
1118 }
1119
cce8e372
SI
1120 $tls_policy->{$destination} = {
1121 destination => $destination,
1b449731
SI
1122 policy => $policy,
1123 };
1124 } else {
1125 $parse_error->('wrong format');
1126 }
1127 }
1128
1129 return $tls_policy;
1130}
1131
1132sub write_tls_policy {
1133 my ($filename, $fh, $tls_policy) = @_;
1134
1135 return if !$tls_policy;
1136
cce8e372
SI
1137 foreach my $destination (sort keys %$tls_policy) {
1138 my $entry = $tls_policy->{$destination};
1b449731 1139 PVE::Tools::safe_print(
cce8e372 1140 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1b449731
SI
1141 }
1142}
1143
959aaeba 1144my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1b449731
SI
1145PVE::INotify::register_file('tls_policy', $tls_policy_map_filename,
1146 \&read_tls_policy,
1147 \&write_tls_policy,
1148 undef, always_call_parser => 1);
959aaeba
DM
1149
1150sub postmap_tls_policy {
1151 PMG::Utils::run_postmap($tls_policy_map_filename);
1152}
1153
cd533938 1154my $transport_map_filename = "/etc/pmg/transport";
3546daf0 1155
3118b703
DM
1156sub postmap_pmg_transport {
1157 PMG::Utils::run_postmap($transport_map_filename);
1158}
1159
8e3b0ef1
SI
1160PVE::JSONSchema::register_format(
1161 'transport-address', \&pmg_verify_transport_address);
1162
1163sub pmg_verify_transport_address {
1164 my ($name, $noerr) = @_;
1165
1166 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1167 return $name;
1168 } elsif (PVE::JSONSchema::pve_verify_address($name, 1)) {
1169 return $name;
1170 } else {
1171 return undef if $noerr;
1172 die "value does not look like a valid address\n";
1173 }
1174}
1175
3546daf0
DM
1176sub read_transport_map {
1177 my ($filename, $fh) = @_;
1178
1179 return [] if !defined($fh);
1180
1181 my $res = {};
1182
3118b703 1183 my $comment = '';
b7c49fec 1184
3546daf0
DM
1185 while (defined(my $line = <$fh>)) {
1186 chomp $line;
1187 next if $line =~ m/^\s*$/;
3118b703
DM
1188 if ($line =~ m/^#(.*)\s*$/) {
1189 $comment = $1;
1190 next;
1191 }
3546daf0 1192
b7c49fec
DM
1193 my $parse_error = sub {
1194 my ($err) = @_;
1195 warn "parse error in '$filename': $line - $err";
1196 $comment = '';
1197 };
1198
10d97956
JZ
1199 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1200 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
3546daf0 1201
22c25daf 1202 eval { pmg_verify_transport_domain_or_email($domain); };
b7c49fec
DM
1203 if (my $err = $@) {
1204 $parse_error->($err);
1205 next;
1206 }
53904163 1207 my $use_mx = 1;
3546daf0
DM
1208 if ($host =~ m/^\[(.*)\]$/) {
1209 $host = $1;
53904163 1210 $use_mx = 0;
3546daf0 1211 }
0280ecd4 1212 $use_mx = 0 if ($protocol eq "lmtp");
3546daf0 1213
8e3b0ef1 1214 eval { pmg_verify_transport_address($host); };
b7c49fec
DM
1215 if (my $err = $@) {
1216 $parse_error->($err);
1217 next;
1218 }
1219
3118b703
DM
1220 my $data = {
1221 domain => $domain,
10d97956 1222 protocol => $protocol,
3118b703
DM
1223 host => $host,
1224 port => $port,
53904163 1225 use_mx => $use_mx,
3118b703
DM
1226 comment => $comment,
1227 };
1228 $res->{$domain} = $data;
1229 $comment = '';
1230 } else {
b7c49fec 1231 $parse_error->('wrong format');
3546daf0
DM
1232 }
1233 }
1234
3118b703 1235 return $res;
3546daf0
DM
1236}
1237
cd533938 1238sub write_transport_map {
3546daf0
DM
1239 my ($filename, $fh, $tmap) = @_;
1240
1241 return if !$tmap;
1242
3118b703
DM
1243 foreach my $domain (sort keys %$tmap) {
1244 my $data = $tmap->{$domain};
3546daf0 1245
3118b703
DM
1246 my $comment = $data->{comment};
1247 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1248 if defined($comment) && $comment !~ m/^\s*$/;
1249
256ff1ba 1250 my $bracket_host = !$data->{use_mx};
ba323310 1251
204fd68b 1252 if ($data->{protocol} eq 'lmtp') {
0280ecd4 1253 $bracket_host = 0;
204fd68b 1254 $data->{protocol} .= ":inet";
10d97956 1255 }
8e3b0ef1 1256 $bracket_host = 1 if $data->{host} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
256ff1ba 1257 my $host = $bracket_host ? "[$data->{host}]" : $data->{host};
0280ecd4 1258
256ff1ba 1259 PVE::Tools::safe_print($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
3546daf0
DM
1260 }
1261}
1262
1263PVE::INotify::register_file('transport', $transport_map_filename,
1264 \&read_transport_map,
cd533938 1265 \&write_transport_map,
3546daf0 1266 undef, always_call_parser => 1);
7e0e6dbe 1267
4ccdc564
DM
1268# config file generation using templates
1269
08a1d1d9
SI
1270sub get_host_dns_info {
1271 my ($self) = @_;
1272
1273 my $dnsinfo = {};
1274 my $nodename = PVE::INotify::nodename();
1275
1276 $dnsinfo->{hostname} = $nodename;
1277 my $resolv = PVE::INotify::read_file('resolvconf');
1278
1279 my $domain = $resolv->{search} // 'localdomain';
1280 $dnsinfo->{domain} = $domain;
1281
1282 $dnsinfo->{fqdn} = "$nodename.$domain";
1283
1284 return $dnsinfo;
1285}
1286
07b3face
DM
1287sub get_template_vars {
1288 my ($self) = @_;
4ccdc564
DM
1289
1290 my $vars = { pmg => $self->get_config() };
1291
08a1d1d9
SI
1292 my $dnsinfo = get_host_dns_info();
1293 $vars->{dns} = $dnsinfo;
1294 my $int_ip = PMG::Cluster::remote_node_ip($dnsinfo->{hostname});
f609bf7f 1295 $vars->{ipconfig}->{int_ip} = $int_ip;
f609bf7f 1296
ba323310
DM
1297 my $transportnets = [];
1298
5e37e665
DM
1299 if (my $tmap = PVE::INotify::read_file('transport')) {
1300 foreach my $domain (sort keys %$tmap) {
1301 my $data = $tmap->{$domain};
1302 my $host = $data->{host};
1303 if ($host =~ m/^$IPV4RE$/) {
1304 push @$transportnets, "$host/32";
8e3b0ef1
SI
1305 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1306 push @$transportnets, "[$1]/128";
5e37e665 1307 }
ba323310
DM
1308 }
1309 }
1310
f609bf7f
DM
1311 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
1312
1313 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1e88a529
DM
1314
1315 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
136e6c92
DM
1316 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1317 push @$mynetworks, "[$1]/$2";
1318 } else {
1319 push @$mynetworks, $int_net_cidr;
1320 }
1e88a529
DM
1321 } else {
1322 if ($int_ip =~ m/^$IPV6RE$/) {
136e6c92 1323 push @$mynetworks, "[$int_ip]/128";
1e88a529
DM
1324 } else {
1325 push @$mynetworks, "$int_ip/32";
1326 }
1327 }
f609bf7f 1328
bef31f06 1329 my $netlist = PVE::INotify::read_file('mynetworks');
b8733ec1 1330 foreach my $cidr (sort keys %$netlist) {
136e6c92
DM
1331 if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1332 push @$mynetworks, "[$1]/$2";
1333 } else {
1334 push @$mynetworks, $cidr;
1335 }
1336 }
6d473888
DM
1337
1338 push @$mynetworks, @$transportnets;
1339
f609bf7f
DM
1340 # add default relay to mynetworks
1341 if (my $relay = $self->get('mail', 'relay')) {
ba323310 1342 if ($relay =~ m/^$IPV4RE$/) {
f609bf7f 1343 push @$mynetworks, "$relay/32";
ba323310 1344 } elsif ($relay =~ m/^$IPV6RE$/) {
f609bf7f
DM
1345 push @$mynetworks, "[$relay]/128";
1346 } else {
66af5153 1347 # DNS name - do nothing ?
f609bf7f
DM
1348 }
1349 }
1350
1351 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1352
20125a71
DM
1353 # normalize dnsbl_sites
1354 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1355 if (scalar(@dnsbl_sites)) {
1356 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1357 }
1358
11247512
AP
1359 $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
1360
f609bf7f
DM
1361 my $usepolicy = 0;
1362 $usepolicy = 1 if $self->get('mail', 'greylist') ||
aa7c3745 1363 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
f609bf7f
DM
1364 $vars->{postfix}->{usepolicy} = $usepolicy;
1365
2664d3cb
DM
1366 if ($int_ip =~ m/^$IPV6RE$/) {
1367 $vars->{postfix}->{int_ip} = "[$int_ip]";
1368 } else {
1369 $vars->{postfix}->{int_ip} = $int_ip;
1370 }
1371
08a1d1d9 1372 my $wlbr = $dnsinfo->{fqdn};
ed5fa523
DM
1373 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1374 $wlbr .= " $r"
1375 }
1376 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1377
11081cf6
DM
1378 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1379 eval {
1380 my $uri = URI->new($proxy);
1381 my $host = $uri->host;
1382 my $port = $uri->port // 8080;
1383 if ($host) {
1384 my $data = { host => $host, port => $port };
1385 if (my $ui = $uri->userinfo) {
1386 my ($username, $pw) = split(/:/, $ui, 2);
1387 $data->{username} = $username;
1388 $data->{password} = $pw if defined($pw);
1389 }
1390 $vars->{proxy} = $data;
1391 }
1392 };
1393 warn "parse http_proxy failed - $@" if $@;
1394 }
2005e4b3 1395 $vars->{postgres}->{version} = PMG::Utils::get_pg_server_version();
11081cf6 1396
07b3face
DM
1397 return $vars;
1398}
1399
310daf18
DM
1400# use one global TT cache
1401our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1402
1403my $template_toolkit;
1404
1405sub get_template_toolkit {
1406
1407 return $template_toolkit if $template_toolkit;
1408
1409 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1410
1411 return $template_toolkit;
1412}
1413
07b3face
DM
1414# rewrite file from template
1415# return true if file has changed
1416sub rewrite_config_file {
1417 my ($self, $tmplname, $dstfn) = @_;
1418
1419 my $demo = $self->get('admin', 'demo');
1420
07b3face 1421 if ($demo) {
310daf18
DM
1422 my $demosrc = "$tmplname.demo";
1423 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
07b3face
DM
1424 }
1425
c248d69f 1426 my ($perm, $uid, $gid);
07b3face 1427
d1156caa 1428 if ($dstfn eq '/etc/clamav/freshclam.conf') {
07b3face
DM
1429 # needed if file contains a HTTPProxyPasswort
1430
1431 $uid = getpwnam('clamav');
1432 $gid = getgrnam('adm');
1433 $perm = 0600;
1434 }
1435
310daf18 1436 my $tt = get_template_toolkit();
07b3face
DM
1437
1438 my $vars = $self->get_template_vars();
1439
c248d69f 1440 my $output = '';
07b3face 1441
310daf18 1442 $tt->process($tmplname, $vars, \$output) ||
60f82a46 1443 die $tt->error() . "\n";
07b3face
DM
1444
1445 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1446
1447 return 0 if defined($old) && ($old eq $output); # no change
1448
1449 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1450
1451 if (defined($uid) && defined($gid)) {
1452 chown($uid, $gid, $dstfn);
1453 }
1454
1455 return 1;
4ccdc564
DM
1456}
1457
9123cab5
DM
1458# rewrite spam configuration
1459sub rewrite_config_spam {
1460 my ($self) = @_;
1461
1462 my $use_awl = $self->get('spam', 'use_awl');
1463 my $use_bayes = $self->get('spam', 'use_bayes');
1464 my $use_razor = $self->get('spam', 'use_razor');
1465
17424665
DM
1466 my $changes = 0;
1467
9123cab5 1468 # delete AW and bayes databases if those features are disabled
17424665
DM
1469 if (!$use_awl) {
1470 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1471 }
1472
9123cab5 1473 if (!$use_bayes) {
17424665
DM
1474 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1475 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1476 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
9123cab5
DM
1477 }
1478
f9d8c305 1479 # make sure we have the custom SA files (else cluster sync fails)
9123cab5 1480 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
f9d8c305 1481 IO::File->new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
9123cab5 1482
17424665
DM
1483 $changes = 1 if $self->rewrite_config_file(
1484 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1485
1486 $changes = 1 if $self->rewrite_config_file(
1487 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1488
1489 $changes = 1 if $self->rewrite_config_file(
1490 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1491
1492 $changes = 1 if $self->rewrite_config_file(
1493 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
9123cab5
DM
1494
1495 if ($use_razor) {
1496 mkdir "/root/.razor";
17424665
DM
1497
1498 $changes = 1 if $self->rewrite_config_file(
1499 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1500
9123cab5
DM
1501 if (! -e '/root/.razor/identity') {
1502 eval {
1503 my $timeout = 30;
17424665
DM
1504 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1505 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
9123cab5
DM
1506 };
1507 my $err = $@;
b902c0b8 1508 syslog('info', "registering razor failed: $err") if $err;
9123cab5
DM
1509 }
1510 }
17424665
DM
1511
1512 return $changes;
9123cab5
DM
1513}
1514
ac5d1312
DM
1515# rewrite ClamAV configuration
1516sub rewrite_config_clam {
1517 my ($self) = @_;
1518
17424665
DM
1519 return $self->rewrite_config_file(
1520 'clamd.conf.in', '/etc/clamav/clamd.conf');
1521}
1522
1523sub rewrite_config_freshclam {
1524 my ($self) = @_;
1525
1526 return $self->rewrite_config_file(
1527 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
ac5d1312
DM
1528}
1529
86737f12
DM
1530sub rewrite_config_postgres {
1531 my ($self) = @_;
1532
2005e4b3
SI
1533 my $pg_maj_version = PMG::Utils::get_pg_server_version();
1534 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
86737f12 1535
17424665
DM
1536 my $changes = 0;
1537
1538 $changes = 1 if $self->rewrite_config_file(
1539 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1540
1541 $changes = 1 if $self->rewrite_config_file(
1542 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1543
1544 return $changes;
86737f12
DM
1545}
1546
1547# rewrite /root/.forward
1548sub rewrite_dot_forward {
1549 my ($self) = @_;
1550
c248d69f 1551 my $dstfn = '/root/.forward';
86737f12 1552
0bb9a01a 1553 my $email = $self->get('admin', 'email');
c248d69f 1554
e14fda7a 1555 my $output = '';
86737f12 1556 if ($email && $email =~ m/\s*(\S+)\s*/) {
c248d69f 1557 $output = "$1\n";
86737f12
DM
1558 } else {
1559 # empty .forward does not forward mails (see man local)
1560 }
17424665 1561
c248d69f
DM
1562 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1563
1564 return 0 if defined($old) && ($old eq $output); # no change
1565
1566 PVE::Tools::file_set_contents($dstfn, $output);
1567
1568 return 1;
86737f12
DM
1569}
1570
d15630a9
DM
1571my $write_smtp_whitelist = sub {
1572 my ($filename, $data, $action) = @_;
1573
1574 $action = 'OK' if !$action;
1575
1576 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1577 if -f $filename;
1578
1579 my $new = '';
1580 foreach my $k (sort keys %$data) {
1581 $new .= "$k $action\n";
1582 }
1583
1584 return 0 if defined($old) && ($old eq $new); # no change
1585
1586 PVE::Tools::file_set_contents($filename, $new);
1587
1588 PMG::Utils::run_postmap($filename);
1589
1590 return 1;
1591};
1592
f9967a49 1593sub rewrite_postfix_whitelist {
d15630a9
DM
1594 my ($rulecache) = @_;
1595
1596 # see man page for regexp_table for postfix regex table format
1597
1598 # we use a hash to avoid duplicate entries in regex tables
1599 my $tolist = {};
1600 my $fromlist = {};
1601 my $clientlist = {};
1602
1603 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1604 my $oclass = ref($obj);
1605 if ($oclass eq 'PMG::RuleDB::Receiver') {
1606 my $addr = PMG::Utils::quote_regex($obj->{address});
1607 $tolist->{"/^$addr\$/"} = 1;
1608 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1609 my $addr = PMG::Utils::quote_regex($obj->{address});
1610 $tolist->{"/^.+\@$addr\$/"} = 1;
1611 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1612 my $addr = $obj->{address};
1613 $addr =~ s|/|\\/|g;
1614 $tolist->{"/^$addr\$/"} = 1;
1615 }
1616 }
1617
1618 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1619 my $oclass = ref($obj);
1620 my $addr = PMG::Utils::quote_regex($obj->{address});
1621 if ($oclass eq 'PMG::RuleDB::EMail') {
1622 my $addr = PMG::Utils::quote_regex($obj->{address});
1623 $fromlist->{"/^$addr\$/"} = 1;
1624 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1625 my $addr = PMG::Utils::quote_regex($obj->{address});
1626 $fromlist->{"/^.+\@$addr\$/"} = 1;
1627 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1628 my $addr = $obj->{address};
1629 $addr =~ s|/|\\/|g;
1630 $fromlist->{"/^$addr\$/"} = 1;
1631 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1632 $clientlist->{$obj->{address}} = 1;
1633 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1634 $clientlist->{$obj->{address}} = 1;
1635 }
1636 }
1637
1638 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1639 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1640 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1641 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1642};
1643
f609bf7f
DM
1644# rewrite /etc/postfix/*
1645sub rewrite_config_postfix {
d15630a9 1646 my ($self, $rulecache) = @_;
f609bf7f 1647
3546daf0 1648 # make sure we have required files (else postfix start fails)
3546daf0 1649 IO::File->new($transport_map_filename, 'a', 0644);
f609bf7f 1650
17424665
DM
1651 my $changes = 0;
1652
f609bf7f
DM
1653 if ($self->get('mail', 'tls')) {
1654 eval {
bc44eb02 1655 PMG::Utils::gen_proxmox_tls_cert();
f609bf7f 1656 };
b902c0b8 1657 syslog ('info', "generating certificate failed: $@") if $@;
f609bf7f
DM
1658 }
1659
17424665
DM
1660 $changes = 1 if $self->rewrite_config_file(
1661 'main.cf.in', '/etc/postfix/main.cf');
1662
1663 $changes = 1 if $self->rewrite_config_file(
1664 'master.cf.in', '/etc/postfix/master.cf');
1665
a0d4ce8d
DM
1666 # make sure we have required files (else postfix start fails)
1667 # Note: postmap need a valid /etc/postfix/main.cf configuration
1668 postmap_pmg_domains();
1669 postmap_pmg_transport();
1670 postmap_tls_policy();
1671
f9967a49 1672 rewrite_postfix_whitelist($rulecache) if $rulecache;
d15630a9 1673
f609bf7f
DM
1674 # make sure aliases.db is up to date
1675 system('/usr/bin/newaliases');
17424665
DM
1676
1677 return $changes;
f609bf7f
DM
1678}
1679
e5b31a61
SI
1680#parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1681my $pmg_service_params = {
71c68eba
SI
1682 mail => {
1683 hide_received => 1,
1684 ndr_on_block => 1,
1685 },
e4b38221
SI
1686 admin => {
1687 dkim_selector => 1,
1688 dkim_sign => 1,
1689 dkim_sign_all_mail => 1,
1690 },
e5b31a61
SI
1691};
1692
1693my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1694my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1695
1696sub dump_smtp_filter_config {
1697 my ($self) = @_;
1698
1699 my $conf = '';
1700 my $val;
1701 foreach my $sec (sort keys %$pmg_service_params) {
1702 my $conf_sec = $self->{ids}->{$sec} // {};
1703 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1704 $val = $conf_sec->{$key};
1705 $conf .= "$sec.$key:$val\n" if defined($val);
1706 }
1707 }
1708
1709 return $conf;
1710}
1711
1712sub compare_smtp_filter_config {
1713 my ($self) = @_;
1714
1715 my $ret = 0;
1716 my $old;
1717 eval {
1718 $old = PVE::Tools::file_get_contents($smtp_filter_cfg);
1719 };
1720
1721 if (my $err = $@) {
1722 syslog ('warning', "reloading pmg-smtp-filter: $err");
1723 $ret = 1;
1724 } else {
1725 my $new = $self->dump_smtp_filter_config();
1726 $ret = 1 if $old ne $new;
1727 }
1728
1729 $self->write_smtp_filter_config() if $ret;
1730
1731 return $ret;
1732}
1733
1734# writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1735# on config change
1736sub write_smtp_filter_config {
1737 my ($self) = @_;
1738
1739 PVE::Tools::lock_file($smtp_filter_cfg_lock, undef, sub {
1740 PVE::Tools::file_set_contents($smtp_filter_cfg,
1741 $self->dump_smtp_filter_config());
1742 });
1743
1744 die $@ if $@;
1745}
1746
f983300f 1747sub rewrite_config {
d15630a9 1748 my ($self, $rulecache, $restart_services, $force_restart) = @_;
c248d69f 1749
798df412
DM
1750 $force_restart = {} if ! $force_restart;
1751
e5bd6522
TL
1752 my $log_restart = sub {
1753 syslog ('info', "configuration change detected for '$_[0]', restarting");
1754 };
1755
d15630a9 1756 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
798df412 1757 $force_restart->{postfix}) {
e5bd6522 1758 $log_restart->('postfix');
2473cb81 1759 PMG::Utils::service_cmd('postfix', 'reload');
c248d69f
DM
1760 }
1761
1762 if ($self->rewrite_dot_forward() && $restart_services) {
1763 # no need to restart anything
1764 }
1765
1766 if ($self->rewrite_config_postgres() && $restart_services) {
1767 # do nothing (too many side effects)?
1768 # does not happen anyways, because config does not change.
1769 }
f983300f 1770
798df412
DM
1771 if (($self->rewrite_config_spam() && $restart_services) ||
1772 $force_restart->{spam}) {
e5bd6522 1773 $log_restart->('pmg-smtp-filter');
c248d69f
DM
1774 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1775 }
1776
798df412
DM
1777 if (($self->rewrite_config_clam() && $restart_services) ||
1778 $force_restart->{clam}) {
e5bd6522 1779 $log_restart->('clamav-daemon');
8f87fe74 1780 PMG::Utils::service_cmd('clamav-daemon', 'restart');
c248d69f
DM
1781 }
1782
798df412
DM
1783 if (($self->rewrite_config_freshclam() && $restart_services) ||
1784 $force_restart->{freshclam}) {
e5bd6522 1785 $log_restart->('clamav-freshclam');
8f87fe74 1786 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
c248d69f 1787 }
e5b31a61
SI
1788
1789 if (($self->compare_smtp_filter_config() && $restart_services) ||
1790 $force_restart->{spam}) {
1791 syslog ('info', "scheduled reload for pmg-smtp-filter");
1792 PMG::Utils::reload_smtp_filter();
1793 }
f983300f
DM
1794}
1795
7e0e6dbe 17961;