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