]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/Config.pm
4634bd5fc49457ced65515703ba720d1cd1bc5ec
[pmg-api.git] / src / PMG / Config.pm
1 package PMG::Config::Base;
2
3 use strict;
4 use warnings;
5 use URI;
6 use Data::Dumper;
7
8 use PVE::Tools;
9 use PVE::JSONSchema qw(get_standard_option);
10 use PVE::SectionConfig;
11
12 use base qw(PVE::SectionConfig);
13
14 my $defaultData = {
15 propertyList => {
16 type => { description => "Section type." },
17 section => {
18 description => "Section ID.",
19 type => 'string', format => 'pve-configid',
20 },
21 },
22 };
23
24 sub private {
25 return $defaultData;
26 }
27
28 sub format_section_header {
29 my ($class, $type, $sectionId) = @_;
30
31 die "internal error ($type ne $sectionId)" if $type ne $sectionId;
32
33 return "section: $type\n";
34 }
35
36
37 sub parse_section_header {
38 my ($class, $line) = @_;
39
40 if ($line =~ m/^section:\s*(\S+)\s*$/) {
41 my $section = $1;
42 my $errmsg = undef; # set if you want to skip whole section
43 eval { PVE::JSONSchema::pve_verify_configid($section); };
44 $errmsg = $@ if $@;
45 my $config = {}; # to return additional attributes
46 return ($section, $section, $errmsg, $config);
47 }
48 return undef;
49 }
50
51 package PMG::Config::Admin;
52
53 use strict;
54 use warnings;
55
56 use base qw(PMG::Config::Base);
57
58 sub type {
59 return 'admin';
60 }
61
62 sub properties {
63 return {
64 advfilter => {
65 description => "Use advanced filters for statistic.",
66 type => 'boolean',
67 default => 1,
68 },
69 dailyreport => {
70 description => "Send daily reports.",
71 type => 'boolean',
72 default => 1,
73 },
74 statlifetime => {
75 description => "User Statistics Lifetime (days)",
76 type => 'integer',
77 default => 7,
78 minimum => 1,
79 },
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',
89 },
90 http_proxy => {
91 description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
92 type => 'string',
93 pattern => "http://.*",
94 },
95 avast => {
96 description => "Use Avast Virus Scanner (/usr/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.",
97 type => 'boolean',
98 default => 0,
99 },
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 },
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 },
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 },
129 };
130 }
131
132 sub options {
133 return {
134 advfilter => { optional => 1 },
135 avast => { optional => 1 },
136 clamav => { optional => 1 },
137 statlifetime => { optional => 1 },
138 dailyreport => { optional => 1 },
139 demo => { optional => 1 },
140 email => { optional => 1 },
141 http_proxy => { optional => 1 },
142 custom_check => { optional => 1 },
143 custom_check_path => { optional => 1 },
144 dkim_sign => { optional => 1 },
145 dkim_sign_all_mail => { optional => 1 },
146 dkim_selector => { optional => 1 },
147 };
148 }
149
150 package PMG::Config::Spam;
151
152 use strict;
153 use warnings;
154
155 use base qw(PMG::Config::Base);
156
157 sub type {
158 return 'spam';
159 }
160
161 sub properties {
162 return {
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 },
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 },
184 wl_bounce_relays => {
185 description => "Whitelist legitimate bounce relays.",
186 type => 'string',
187 },
188 clamav_heuristic_score => {
189 description => "Score for ClamAV heuristics (Encrypted Archives/Documents, Google Safe Browsing database, PhishingScanURLs, ...).",
190 type => 'integer',
191 minimum => 0,
192 maximum => 1000,
193 default => 3,
194 },
195 bounce_score => {
196 description => "Additional score for bounce mails.",
197 type => 'integer',
198 minimum => 0,
199 maximum => 1000,
200 default => 0,
201 },
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',
210 minimum => 64,
211 default => 256*1024,
212 },
213 };
214 }
215
216 sub options {
217 return {
218 use_awl => { optional => 1 },
219 use_razor => { optional => 1 },
220 wl_bounce_relays => { optional => 1 },
221 languages => { optional => 1 },
222 use_bayes => { optional => 1 },
223 clamav_heuristic_score => { optional => 1 },
224 bounce_score => { optional => 1 },
225 rbl_checks => { optional => 1 },
226 maxspamsize => { optional => 1 },
227 };
228 }
229
230 package PMG::Config::SpamQuarantine;
231
232 use strict;
233 use warnings;
234
235 use base qw(PMG::Config::Base);
236
237 sub type {
238 return 'spamquar';
239 }
240
241 sub 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',
258 enum => [qw(none short verbose custom)],
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,
270 },
271 hostname => {
272 description => "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
273 type => 'string', format => 'address',
274 },
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 },
288 mailfrom => {
289 description => "Text for 'From' header in daily spam report mails.",
290 type => 'string',
291 },
292 };
293 }
294
295 sub options {
296 return {
297 mailfrom => { optional => 1 },
298 hostname => { optional => 1 },
299 lifetime => { optional => 1 },
300 authmode => { optional => 1 },
301 reportstyle => { optional => 1 },
302 viewimages => { optional => 1 },
303 allowhrefs => { optional => 1 },
304 port => { optional => 1 },
305 protocol => { optional => 1 },
306 };
307 }
308
309 package PMG::Config::VirusQuarantine;
310
311 use strict;
312 use warnings;
313
314 use base qw(PMG::Config::Base);
315
316 sub type {
317 return 'virusquar';
318 }
319
320 sub properties {
321 return {};
322 }
323
324 sub options {
325 return {
326 lifetime => { optional => 1 },
327 viewimages => { optional => 1 },
328 allowhrefs => { optional => 1 },
329 };
330 }
331
332 package PMG::Config::ClamAV;
333
334 use strict;
335 use warnings;
336
337 use base qw(PMG::Config::Base);
338
339 sub type {
340 return 'clamav';
341 }
342
343 sub properties {
344 return {
345 dbmirror => {
346 description => "ClamAV database mirror server.",
347 type => 'string',
348 default => 'database.clamav.net',
349 },
350 archiveblockencrypted => {
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'.",
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.",
357 type => 'integer',
358 minimum => 1,
359 default => 5,
360 },
361 archivemaxfiles => {
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.",
363 type => 'integer',
364 minimum => 0,
365 default => 1000,
366 },
367 archivemaxsize => {
368 description => "Files larger than this limit (in bytes) won't be scanned.",
369 type => 'integer',
370 minimum => 1000000,
371 default => 25000000,
372 },
373 maxscansize => {
374 description => "Sets the maximum amount of data (in bytes) to be scanned for each input file.",
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 },
385 safebrowsing => {
386 description => "Enables support for Google Safe Browsing.",
387 type => 'boolean',
388 default => 1
389 },
390 };
391 }
392
393 sub options {
394 return {
395 archiveblockencrypted => { optional => 1 },
396 archivemaxrec => { optional => 1 },
397 archivemaxfiles => { optional => 1 },
398 archivemaxsize => { optional => 1 },
399 maxscansize => { optional => 1 },
400 dbmirror => { optional => 1 },
401 maxcccount => { optional => 1 },
402 safebrowsing => { optional => 1 },
403 };
404 }
405
406 package PMG::Config::Mail;
407
408 use strict;
409 use warnings;
410
411 use PVE::ProcFSTools;
412
413 use base qw(PMG::Config::Base);
414
415 sub type {
416 return 'mail';
417 }
418
419 my $physicalmem = 0;
420 sub 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
430 sub 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
443 sub 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
455 sub 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 }
462
463 sub properties {
464 return {
465 int_port => {
466 description => "SMTP port number for outgoing mail (trusted).",
467 type => 'integer',
468 minimum => 1,
469 maximum => 65535,
470 default => 26,
471 },
472 ext_port => {
473 description => "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
474 type => 'integer',
475 minimum => 1,
476 maximum => 65535,
477 default => 25,
478 },
479 relay => {
480 description => "The default mail delivery transport (incoming mails).",
481 type => 'string', format => 'address',
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.",
497 type => 'string', format => 'address',
498 },
499 smarthostport => {
500 description => "SMTP port number for smarthost.",
501 type => 'integer',
502 minimum => 1,
503 maximum => 65535,
504 default => 25,
505 },
506 banner => {
507 description => "ESMTP banner.",
508 type => 'string',
509 maxLength => 1024,
510 default => 'ESMTP Proxmox',
511 },
512 max_filters => {
513 description => "Maximum number of pmg-smtp-filter processes.",
514 type => 'integer',
515 minimum => 3,
516 maximum => 40,
517 default => get_max_filters(),
518 },
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 },
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 },
558 hide_received => {
559 description => "Hide received header in outgoing mails.",
560 type => 'boolean',
561 default => 0,
562 },
563 maxsize => {
564 description => "Maximum email size. Larger mails are rejected.",
565 type => 'integer',
566 minimum => 1024,
567 default => 1024*1024*10,
568 },
569 dwarning => {
570 description => "SMTP delay warning time (in hours).",
571 type => 'integer',
572 minimum => 0,
573 default => 4,
574 },
575 tls => {
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.",
587 type => 'boolean',
588 default => 0,
589 },
590 spf => {
591 description => "Use Sender Policy Framework.",
592 type => 'boolean',
593 default => 1,
594 },
595 greylist => {
596 description => "Use Greylisting.",
597 type => 'boolean',
598 default => 1,
599 },
600 helotests => {
601 description => "Use SMTP HELO tests.",
602 type => 'boolean',
603 default => 0,
604 },
605 rejectunknown => {
606 description => "Reject unknown clients.",
607 type => 'boolean',
608 default => 0,
609 },
610 rejectunknownsender => {
611 description => "Reject unknown senders.",
612 type => 'boolean',
613 default => 0,
614 },
615 verifyreceivers => {
616 description => "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
617 type => 'string',
618 enum => ['450', '550'],
619 },
620 dnsbl_sites => {
621 description => "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
622 type => 'string', format => 'dnsbl-entry-list',
623 },
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 },
630 };
631 }
632
633 sub options {
634 return {
635 int_port => { optional => 1 },
636 ext_port => { optional => 1 },
637 smarthost => { optional => 1 },
638 smarthostport => { optional => 1 },
639 relay => { optional => 1 },
640 relayport => { optional => 1 },
641 relaynomx => { optional => 1 },
642 dwarning => { optional => 1 },
643 max_smtpd_in => { optional => 1 },
644 max_smtpd_out => { optional => 1 },
645 greylist => { optional => 1 },
646 helotests => { optional => 1 },
647 tls => { optional => 1 },
648 tlslog => { optional => 1 },
649 tlsheader => { optional => 1 },
650 spf => { optional => 1 },
651 maxsize => { optional => 1 },
652 banner => { optional => 1 },
653 max_filters => { optional => 1 },
654 max_policy => { optional => 1 },
655 hide_received => { optional => 1 },
656 rejectunknown => { optional => 1 },
657 rejectunknownsender => { optional => 1 },
658 conn_count_limit => { optional => 1 },
659 conn_rate_limit => { optional => 1 },
660 message_rate_limit => { optional => 1 },
661 verifyreceivers => { optional => 1 },
662 dnsbl_sites => { optional => 1 },
663 dnsbl_threshold => { optional => 1 },
664 };
665 }
666
667 package PMG::Config;
668
669 use strict;
670 use warnings;
671 use IO::File;
672 use Data::Dumper;
673 use Template;
674
675 use PVE::SafeSyslog;
676 use PVE::Tools qw($IPV4RE $IPV6RE);
677 use PVE::INotify;
678 use PVE::JSONSchema;
679
680 use PMG::Cluster;
681 use PMG::Utils;
682
683 PMG::Config::Admin->register();
684 PMG::Config::Mail->register();
685 PMG::Config::SpamQuarantine->register();
686 PMG::Config::VirusQuarantine->register();
687 PMG::Config::Spam->register();
688 PMG::Config::ClamAV->register();
689
690 # initialize all plugins
691 PMG::Config::Base->init();
692
693 PVE::JSONSchema::register_format(
694 'transport-domain', \&pmg_verify_transport_domain);
695
696 sub pmg_verify_transport_domain {
697 my ($name, $noerr) = @_;
698
699 # like dns-name, but can contain leading dot
700 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
701
702 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
703 return undef if $noerr;
704 die "value does not look like a valid transport domain\n";
705 }
706 return $name;
707 }
708
709 PVE::JSONSchema::register_format(
710 'transport-domain-or-email', \&pmg_verify_transport_domain_or_email);
711
712 sub pmg_verify_transport_domain_or_email {
713 my ($name, $noerr) = @_;
714
715 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
716
717 # email address
718 if ($name =~ m/^(?:[^\s\/\@]+\@)(${namere}\.)*${namere}$/) {
719 return $name;
720 }
721
722 # like dns-name, but can contain leading dot
723 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
724 return undef if $noerr;
725 die "value does not look like a valid transport domain or email address\n";
726 }
727 return $name;
728 }
729
730 PVE::JSONSchema::register_format(
731 'dnsbl-entry', \&pmg_verify_dnsbl_entry);
732
733 sub pmg_verify_dnsbl_entry {
734 my ($name, $noerr) = @_;
735
736 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
737 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
738 # we don't implement the ';' separated numbers in pattern, because this
739 # breaks at PVE::JSONSchema::split_list
740 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
741
742 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
743 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
744 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
745 return undef if $noerr;
746 die "value '$name' does not look like a valid dnsbl entry\n";
747 }
748 return $name;
749 }
750
751 sub new {
752 my ($type) = @_;
753
754 my $class = ref($type) || $type;
755
756 my $cfg = PVE::INotify::read_file("pmg.conf");
757
758 return bless $cfg, $class;
759 }
760
761 sub write {
762 my ($self) = @_;
763
764 PVE::INotify::write_file("pmg.conf", $self);
765 }
766
767 my $lockfile = "/var/lock/pmgconfig.lck";
768
769 sub lock_config {
770 my ($code, $errmsg) = @_;
771
772 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
773 if (my $err = $@) {
774 $errmsg ? die "$errmsg: $err" : die $err;
775 }
776 }
777
778 # set section values
779 sub set {
780 my ($self, $section, $key, $value) = @_;
781
782 my $pdata = PMG::Config::Base->private();
783
784 my $plugin = $pdata->{plugins}->{$section};
785 die "no such section '$section'" if !$plugin;
786
787 if (defined($value)) {
788 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
789 $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
790 $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
791 } else {
792 if (defined($self->{ids}->{$section})) {
793 delete $self->{ids}->{$section}->{$key};
794 }
795 }
796
797 return undef;
798 }
799
800 # get section value or default
801 sub get {
802 my ($self, $section, $key, $nodefault) = @_;
803
804 my $pdata = PMG::Config::Base->private();
805 my $pdesc = $pdata->{propertyList}->{$key};
806 die "no such property '$section/$key'\n"
807 if !(defined($pdesc) && defined($pdata->{options}->{$section}) &&
808 defined($pdata->{options}->{$section}->{$key}));
809
810 if (defined($self->{ids}->{$section}) &&
811 defined(my $value = $self->{ids}->{$section}->{$key})) {
812 return $value;
813 }
814
815 return undef if $nodefault;
816
817 return $pdesc->{default};
818 }
819
820 # get a whole section with default value
821 sub get_section {
822 my ($self, $section) = @_;
823
824 my $pdata = PMG::Config::Base->private();
825 return undef if !defined($pdata->{options}->{$section});
826
827 my $res = {};
828
829 foreach my $key (keys %{$pdata->{options}->{$section}}) {
830
831 my $pdesc = $pdata->{propertyList}->{$key};
832
833 if (defined($self->{ids}->{$section}) &&
834 defined(my $value = $self->{ids}->{$section}->{$key})) {
835 $res->{$key} = $value;
836 next;
837 }
838 $res->{$key} = $pdesc->{default};
839 }
840
841 return $res;
842 }
843
844 # get a whole config with default values
845 sub get_config {
846 my ($self) = @_;
847
848 my $pdata = PMG::Config::Base->private();
849
850 my $res = {};
851
852 foreach my $type (keys %{$pdata->{plugins}}) {
853 my $plugin = $pdata->{plugins}->{$type};
854 $res->{$type} = $self->get_section($type);
855 }
856
857 return $res;
858 }
859
860 sub read_pmg_conf {
861 my ($filename, $fh) = @_;
862
863 local $/ = undef; # slurp mode
864
865 my $raw = <$fh> if defined($fh);
866
867 return PMG::Config::Base->parse_config($filename, $raw);
868 }
869
870 sub write_pmg_conf {
871 my ($filename, $fh, $cfg) = @_;
872
873 my $raw = PMG::Config::Base->write_config($filename, $cfg);
874
875 PVE::Tools::safe_print($filename, $fh, $raw);
876 }
877
878 PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
879 \&read_pmg_conf,
880 \&write_pmg_conf,
881 undef, always_call_parser => 1);
882
883 # parsers/writers for other files
884
885 my $domainsfilename = "/etc/pmg/domains";
886
887 sub postmap_pmg_domains {
888 PMG::Utils::run_postmap($domainsfilename);
889 }
890
891 sub read_pmg_domains {
892 my ($filename, $fh) = @_;
893
894 my $domains = {};
895
896 my $comment = '';
897 if (defined($fh)) {
898 while (defined(my $line = <$fh>)) {
899 chomp $line;
900 next if $line =~ m/^\s*$/;
901 if ($line =~ m/^#(.*)\s*$/) {
902 $comment = $1;
903 next;
904 }
905 if ($line =~ m/^(\S+)\s.*$/) {
906 my $domain = $1;
907 $domains->{$domain} = {
908 domain => $domain, comment => $comment };
909 $comment = '';
910 } else {
911 warn "parse error in '$filename': $line\n";
912 $comment = '';
913 }
914 }
915 }
916
917 return $domains;
918 }
919
920 sub write_pmg_domains {
921 my ($filename, $fh, $domains) = @_;
922
923 foreach my $domain (sort keys %$domains) {
924 my $comment = $domains->{$domain}->{comment};
925 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
926 if defined($comment) && $comment !~ m/^\s*$/;
927
928 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
929 }
930 }
931
932 PVE::INotify::register_file('domains', $domainsfilename,
933 \&read_pmg_domains,
934 \&write_pmg_domains,
935 undef, always_call_parser => 1);
936
937 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
938
939 PVE::INotify::register_file('dkimdomains', $dkimdomainsfile,
940 \&read_pmg_domains,
941 \&write_pmg_domains,
942 undef, always_call_parser => 1);
943
944 my $mynetworks_filename = "/etc/pmg/mynetworks";
945
946 sub read_pmg_mynetworks {
947 my ($filename, $fh) = @_;
948
949 my $mynetworks = {};
950
951 my $comment = '';
952 if (defined($fh)) {
953 while (defined(my $line = <$fh>)) {
954 chomp $line;
955 next if $line =~ m/^\s*$/;
956 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
957 my ($network, $prefix_size, $comment) = ($1, $2, $3);
958 my $cidr = "$network/${prefix_size}";
959 $mynetworks->{$cidr} = {
960 cidr => $cidr,
961 network_address => $network,
962 prefix_size => $prefix_size,
963 comment => $comment // '',
964 };
965 } else {
966 warn "parse error in '$filename': $line\n";
967 }
968 }
969 }
970
971 return $mynetworks;
972 }
973
974 sub write_pmg_mynetworks {
975 my ($filename, $fh, $mynetworks) = @_;
976
977 foreach my $cidr (sort keys %$mynetworks) {
978 my $data = $mynetworks->{$cidr};
979 my $comment = $data->{comment} // '*';
980 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
981 }
982 }
983
984 PVE::INotify::register_file('mynetworks', $mynetworks_filename,
985 \&read_pmg_mynetworks,
986 \&write_pmg_mynetworks,
987 undef, always_call_parser => 1);
988
989 PVE::JSONSchema::register_format(
990 'tls-policy', \&pmg_verify_tls_policy);
991
992 # TODO: extend to parse attributes of the policy
993 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
994 sub pmg_verify_tls_policy {
995 my ($policy, $noerr) = @_;
996
997 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
998 return undef if $noerr;
999 die "value '$policy' does not look like a valid tls policy\n";
1000 }
1001 return $policy;
1002 }
1003
1004 PVE::JSONSchema::register_format(
1005 'tls-policy-strict', \&pmg_verify_tls_policy_strict);
1006
1007 sub pmg_verify_tls_policy_strict {
1008 my ($policy, $noerr) = @_;
1009
1010 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1011 return undef if $noerr;
1012 die "value '$policy' does not look like a valid tls policy\n";
1013 }
1014 return $policy;
1015 }
1016
1017 sub read_tls_policy {
1018 my ($filename, $fh) = @_;
1019
1020 return {} if !defined($fh);
1021
1022 my $tls_policy = {};
1023
1024 while (defined(my $line = <$fh>)) {
1025 chomp $line;
1026 next if $line =~ m/^\s*$/;
1027 next if $line =~ m/^#(.*)\s*$/;
1028
1029 my $parse_error = sub {
1030 my ($err) = @_;
1031 die "parse error in '$filename': $line - $err";
1032 };
1033
1034 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1035 my ($domain, $policy) = ($1, $2);
1036
1037 eval {
1038 pmg_verify_transport_domain($domain);
1039 pmg_verify_tls_policy($policy);
1040 };
1041 if (my $err = $@) {
1042 $parse_error->($err);
1043 next;
1044 }
1045
1046 $tls_policy->{$domain} = {
1047 domain => $domain,
1048 policy => $policy,
1049 };
1050 } else {
1051 $parse_error->('wrong format');
1052 }
1053 }
1054
1055 return $tls_policy;
1056 }
1057
1058 sub write_tls_policy {
1059 my ($filename, $fh, $tls_policy) = @_;
1060
1061 return if !$tls_policy;
1062
1063 foreach my $domain (sort keys %$tls_policy) {
1064 my $entry = $tls_policy->{$domain};
1065 PVE::Tools::safe_print(
1066 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1067 }
1068 }
1069
1070 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1071 PVE::INotify::register_file('tls_policy', $tls_policy_map_filename,
1072 \&read_tls_policy,
1073 \&write_tls_policy,
1074 undef, always_call_parser => 1);
1075
1076 sub postmap_tls_policy {
1077 PMG::Utils::run_postmap($tls_policy_map_filename);
1078 }
1079
1080 my $transport_map_filename = "/etc/pmg/transport";
1081
1082 sub postmap_pmg_transport {
1083 PMG::Utils::run_postmap($transport_map_filename);
1084 }
1085
1086 sub read_transport_map {
1087 my ($filename, $fh) = @_;
1088
1089 return [] if !defined($fh);
1090
1091 my $res = {};
1092
1093 my $comment = '';
1094
1095 while (defined(my $line = <$fh>)) {
1096 chomp $line;
1097 next if $line =~ m/^\s*$/;
1098 if ($line =~ m/^#(.*)\s*$/) {
1099 $comment = $1;
1100 next;
1101 }
1102
1103 my $parse_error = sub {
1104 my ($err) = @_;
1105 warn "parse error in '$filename': $line - $err";
1106 $comment = '';
1107 };
1108
1109 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
1110 my ($domain, $host, $port) = ($1, $2, $3);
1111
1112 eval { pmg_verify_transport_domain_or_email($domain); };
1113 if (my $err = $@) {
1114 $parse_error->($err);
1115 next;
1116 }
1117 my $use_mx = 1;
1118 if ($host =~ m/^\[(.*)\]$/) {
1119 $host = $1;
1120 $use_mx = 0;
1121 }
1122
1123 eval { PVE::JSONSchema::pve_verify_address($host); };
1124 if (my $err = $@) {
1125 $parse_error->($err);
1126 next;
1127 }
1128
1129 my $data = {
1130 domain => $domain,
1131 host => $host,
1132 port => $port,
1133 use_mx => $use_mx,
1134 comment => $comment,
1135 };
1136 $res->{$domain} = $data;
1137 $comment = '';
1138 } else {
1139 $parse_error->('wrong format');
1140 }
1141 }
1142
1143 return $res;
1144 }
1145
1146 sub write_transport_map {
1147 my ($filename, $fh, $tmap) = @_;
1148
1149 return if !$tmap;
1150
1151 foreach my $domain (sort keys %$tmap) {
1152 my $data = $tmap->{$domain};
1153
1154 my $comment = $data->{comment};
1155 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1156 if defined($comment) && $comment !~ m/^\s*$/;
1157
1158 my $use_mx = $data->{use_mx};
1159 $use_mx = 0 if $data->{host} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1160
1161 if ($use_mx) {
1162 PVE::Tools::safe_print(
1163 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1164 } else {
1165 PVE::Tools::safe_print(
1166 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1167 }
1168 }
1169 }
1170
1171 PVE::INotify::register_file('transport', $transport_map_filename,
1172 \&read_transport_map,
1173 \&write_transport_map,
1174 undef, always_call_parser => 1);
1175
1176 # config file generation using templates
1177
1178 sub get_template_vars {
1179 my ($self) = @_;
1180
1181 my $vars = { pmg => $self->get_config() };
1182
1183 my $nodename = PVE::INotify::nodename();
1184 my $int_ip = PMG::Cluster::remote_node_ip($nodename);
1185 $vars->{ipconfig}->{int_ip} = $int_ip;
1186
1187 my $transportnets = [];
1188
1189 if (my $tmap = PVE::INotify::read_file('transport')) {
1190 foreach my $domain (sort keys %$tmap) {
1191 my $data = $tmap->{$domain};
1192 my $host = $data->{host};
1193 if ($host =~ m/^$IPV4RE$/) {
1194 push @$transportnets, "$host/32";
1195 } elsif ($host =~ m/^$IPV6RE$/) {
1196 push @$transportnets, "[$host]/128";
1197 }
1198 }
1199 }
1200
1201 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
1202
1203 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1204
1205 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
1206 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1207 push @$mynetworks, "[$1]/$2";
1208 } else {
1209 push @$mynetworks, $int_net_cidr;
1210 }
1211 } else {
1212 if ($int_ip =~ m/^$IPV6RE$/) {
1213 push @$mynetworks, "[$int_ip]/128";
1214 } else {
1215 push @$mynetworks, "$int_ip/32";
1216 }
1217 }
1218
1219 my $netlist = PVE::INotify::read_file('mynetworks');
1220 foreach my $cidr (sort keys %$netlist) {
1221 if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1222 push @$mynetworks, "[$1]/$2";
1223 } else {
1224 push @$mynetworks, $cidr;
1225 }
1226 }
1227
1228 push @$mynetworks, @$transportnets;
1229
1230 # add default relay to mynetworks
1231 if (my $relay = $self->get('mail', 'relay')) {
1232 if ($relay =~ m/^$IPV4RE$/) {
1233 push @$mynetworks, "$relay/32";
1234 } elsif ($relay =~ m/^$IPV6RE$/) {
1235 push @$mynetworks, "[$relay]/128";
1236 } else {
1237 # DNS name - do nothing ?
1238 }
1239 }
1240
1241 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1242
1243 # normalize dnsbl_sites
1244 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1245 if (scalar(@dnsbl_sites)) {
1246 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1247 }
1248
1249 $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
1250
1251 my $usepolicy = 0;
1252 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1253 $self->get('mail', 'spf');
1254 $vars->{postfix}->{usepolicy} = $usepolicy;
1255
1256 if ($int_ip =~ m/^$IPV6RE$/) {
1257 $vars->{postfix}->{int_ip} = "[$int_ip]";
1258 } else {
1259 $vars->{postfix}->{int_ip} = $int_ip;
1260 }
1261
1262 my $resolv = PVE::INotify::read_file('resolvconf');
1263 $vars->{dns}->{hostname} = $nodename;
1264
1265 my $domain = $resolv->{search} // 'localdomain';
1266 $vars->{dns}->{domain} = $domain;
1267
1268 my $wlbr = "$nodename.$domain";
1269 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1270 $wlbr .= " $r"
1271 }
1272 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1273
1274 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1275 eval {
1276 my $uri = URI->new($proxy);
1277 my $host = $uri->host;
1278 my $port = $uri->port // 8080;
1279 if ($host) {
1280 my $data = { host => $host, port => $port };
1281 if (my $ui = $uri->userinfo) {
1282 my ($username, $pw) = split(/:/, $ui, 2);
1283 $data->{username} = $username;
1284 $data->{password} = $pw if defined($pw);
1285 }
1286 $vars->{proxy} = $data;
1287 }
1288 };
1289 warn "parse http_proxy failed - $@" if $@;
1290 }
1291 $vars->{postgres}->{version} = PMG::Utils::get_pg_server_version();
1292
1293 return $vars;
1294 }
1295
1296 # use one global TT cache
1297 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1298
1299 my $template_toolkit;
1300
1301 sub get_template_toolkit {
1302
1303 return $template_toolkit if $template_toolkit;
1304
1305 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1306
1307 return $template_toolkit;
1308 }
1309
1310 # rewrite file from template
1311 # return true if file has changed
1312 sub rewrite_config_file {
1313 my ($self, $tmplname, $dstfn) = @_;
1314
1315 my $demo = $self->get('admin', 'demo');
1316
1317 if ($demo) {
1318 my $demosrc = "$tmplname.demo";
1319 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
1320 }
1321
1322 my ($perm, $uid, $gid);
1323
1324 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1325 # needed if file contains a HTTPProxyPasswort
1326
1327 $uid = getpwnam('clamav');
1328 $gid = getgrnam('adm');
1329 $perm = 0600;
1330 }
1331
1332 my $tt = get_template_toolkit();
1333
1334 my $vars = $self->get_template_vars();
1335
1336 my $output = '';
1337
1338 $tt->process($tmplname, $vars, \$output) ||
1339 die $tt->error() . "\n";
1340
1341 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1342
1343 return 0 if defined($old) && ($old eq $output); # no change
1344
1345 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1346
1347 if (defined($uid) && defined($gid)) {
1348 chown($uid, $gid, $dstfn);
1349 }
1350
1351 return 1;
1352 }
1353
1354 # rewrite spam configuration
1355 sub rewrite_config_spam {
1356 my ($self) = @_;
1357
1358 my $use_awl = $self->get('spam', 'use_awl');
1359 my $use_bayes = $self->get('spam', 'use_bayes');
1360 my $use_razor = $self->get('spam', 'use_razor');
1361
1362 my $changes = 0;
1363
1364 # delete AW and bayes databases if those features are disabled
1365 if (!$use_awl) {
1366 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1367 }
1368
1369 if (!$use_bayes) {
1370 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1371 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1372 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1373 }
1374
1375 # make sure we have a custom.cf file (else cluster sync fails)
1376 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1377
1378 $changes = 1 if $self->rewrite_config_file(
1379 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1380
1381 $changes = 1 if $self->rewrite_config_file(
1382 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1383
1384 $changes = 1 if $self->rewrite_config_file(
1385 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1386
1387 $changes = 1 if $self->rewrite_config_file(
1388 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1389
1390 if ($use_razor) {
1391 mkdir "/root/.razor";
1392
1393 $changes = 1 if $self->rewrite_config_file(
1394 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1395
1396 if (! -e '/root/.razor/identity') {
1397 eval {
1398 my $timeout = 30;
1399 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1400 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
1401 };
1402 my $err = $@;
1403 syslog('info', "registering razor failed: $err") if $err;
1404 }
1405 }
1406
1407 return $changes;
1408 }
1409
1410 # rewrite ClamAV configuration
1411 sub rewrite_config_clam {
1412 my ($self) = @_;
1413
1414 return $self->rewrite_config_file(
1415 'clamd.conf.in', '/etc/clamav/clamd.conf');
1416 }
1417
1418 sub rewrite_config_freshclam {
1419 my ($self) = @_;
1420
1421 return $self->rewrite_config_file(
1422 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1423 }
1424
1425 sub rewrite_config_postgres {
1426 my ($self) = @_;
1427
1428 my $pg_maj_version = PMG::Utils::get_pg_server_version();
1429 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1430
1431 my $changes = 0;
1432
1433 $changes = 1 if $self->rewrite_config_file(
1434 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1435
1436 $changes = 1 if $self->rewrite_config_file(
1437 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1438
1439 return $changes;
1440 }
1441
1442 # rewrite /root/.forward
1443 sub rewrite_dot_forward {
1444 my ($self) = @_;
1445
1446 my $dstfn = '/root/.forward';
1447
1448 my $email = $self->get('admin', 'email');
1449
1450 my $output = '';
1451 if ($email && $email =~ m/\s*(\S+)\s*/) {
1452 $output = "$1\n";
1453 } else {
1454 # empty .forward does not forward mails (see man local)
1455 }
1456
1457 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1458
1459 return 0 if defined($old) && ($old eq $output); # no change
1460
1461 PVE::Tools::file_set_contents($dstfn, $output);
1462
1463 return 1;
1464 }
1465
1466 my $write_smtp_whitelist = sub {
1467 my ($filename, $data, $action) = @_;
1468
1469 $action = 'OK' if !$action;
1470
1471 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1472 if -f $filename;
1473
1474 my $new = '';
1475 foreach my $k (sort keys %$data) {
1476 $new .= "$k $action\n";
1477 }
1478
1479 return 0 if defined($old) && ($old eq $new); # no change
1480
1481 PVE::Tools::file_set_contents($filename, $new);
1482
1483 PMG::Utils::run_postmap($filename);
1484
1485 return 1;
1486 };
1487
1488 sub rewrite_postfix_whitelist {
1489 my ($rulecache) = @_;
1490
1491 # see man page for regexp_table for postfix regex table format
1492
1493 # we use a hash to avoid duplicate entries in regex tables
1494 my $tolist = {};
1495 my $fromlist = {};
1496 my $clientlist = {};
1497
1498 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1499 my $oclass = ref($obj);
1500 if ($oclass eq 'PMG::RuleDB::Receiver') {
1501 my $addr = PMG::Utils::quote_regex($obj->{address});
1502 $tolist->{"/^$addr\$/"} = 1;
1503 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1504 my $addr = PMG::Utils::quote_regex($obj->{address});
1505 $tolist->{"/^.+\@$addr\$/"} = 1;
1506 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1507 my $addr = $obj->{address};
1508 $addr =~ s|/|\\/|g;
1509 $tolist->{"/^$addr\$/"} = 1;
1510 }
1511 }
1512
1513 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1514 my $oclass = ref($obj);
1515 my $addr = PMG::Utils::quote_regex($obj->{address});
1516 if ($oclass eq 'PMG::RuleDB::EMail') {
1517 my $addr = PMG::Utils::quote_regex($obj->{address});
1518 $fromlist->{"/^$addr\$/"} = 1;
1519 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1520 my $addr = PMG::Utils::quote_regex($obj->{address});
1521 $fromlist->{"/^.+\@$addr\$/"} = 1;
1522 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1523 my $addr = $obj->{address};
1524 $addr =~ s|/|\\/|g;
1525 $fromlist->{"/^$addr\$/"} = 1;
1526 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1527 $clientlist->{$obj->{address}} = 1;
1528 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1529 $clientlist->{$obj->{address}} = 1;
1530 }
1531 }
1532
1533 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1534 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1535 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1536 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1537 };
1538
1539 # rewrite /etc/postfix/*
1540 sub rewrite_config_postfix {
1541 my ($self, $rulecache) = @_;
1542
1543 # make sure we have required files (else postfix start fails)
1544 IO::File->new($transport_map_filename, 'a', 0644);
1545
1546 my $changes = 0;
1547
1548 if ($self->get('mail', 'tls')) {
1549 eval {
1550 PMG::Utils::gen_proxmox_tls_cert();
1551 };
1552 syslog ('info', "generating certificate failed: $@") if $@;
1553 }
1554
1555 $changes = 1 if $self->rewrite_config_file(
1556 'main.cf.in', '/etc/postfix/main.cf');
1557
1558 $changes = 1 if $self->rewrite_config_file(
1559 'master.cf.in', '/etc/postfix/master.cf');
1560
1561 # make sure we have required files (else postfix start fails)
1562 # Note: postmap need a valid /etc/postfix/main.cf configuration
1563 postmap_pmg_domains();
1564 postmap_pmg_transport();
1565 postmap_tls_policy();
1566
1567 rewrite_postfix_whitelist($rulecache) if $rulecache;
1568
1569 # make sure aliases.db is up to date
1570 system('/usr/bin/newaliases');
1571
1572 return $changes;
1573 }
1574
1575 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1576 my $pmg_service_params = {
1577 mail => { hide_received => 1 },
1578 admin => {
1579 dkim_selector => 1,
1580 dkim_sign => 1,
1581 dkim_sign_all_mail => 1,
1582 },
1583 };
1584
1585 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1586 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1587
1588 sub dump_smtp_filter_config {
1589 my ($self) = @_;
1590
1591 my $conf = '';
1592 my $val;
1593 foreach my $sec (sort keys %$pmg_service_params) {
1594 my $conf_sec = $self->{ids}->{$sec} // {};
1595 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1596 $val = $conf_sec->{$key};
1597 $conf .= "$sec.$key:$val\n" if defined($val);
1598 }
1599 }
1600
1601 return $conf;
1602 }
1603
1604 sub compare_smtp_filter_config {
1605 my ($self) = @_;
1606
1607 my $ret = 0;
1608 my $old;
1609 eval {
1610 $old = PVE::Tools::file_get_contents($smtp_filter_cfg);
1611 };
1612
1613 if (my $err = $@) {
1614 syslog ('warning', "reloading pmg-smtp-filter: $err");
1615 $ret = 1;
1616 } else {
1617 my $new = $self->dump_smtp_filter_config();
1618 $ret = 1 if $old ne $new;
1619 }
1620
1621 $self->write_smtp_filter_config() if $ret;
1622
1623 return $ret;
1624 }
1625
1626 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1627 # on config change
1628 sub write_smtp_filter_config {
1629 my ($self) = @_;
1630
1631 PVE::Tools::lock_file($smtp_filter_cfg_lock, undef, sub {
1632 PVE::Tools::file_set_contents($smtp_filter_cfg,
1633 $self->dump_smtp_filter_config());
1634 });
1635
1636 die $@ if $@;
1637 }
1638
1639 sub rewrite_config {
1640 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1641
1642 $force_restart = {} if ! $force_restart;
1643
1644 my $log_restart = sub {
1645 syslog ('info', "configuration change detected for '$_[0]', restarting");
1646 };
1647
1648 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1649 $force_restart->{postfix}) {
1650 $log_restart->('postfix');
1651 PMG::Utils::service_cmd('postfix', 'reload');
1652 }
1653
1654 if ($self->rewrite_dot_forward() && $restart_services) {
1655 # no need to restart anything
1656 }
1657
1658 if ($self->rewrite_config_postgres() && $restart_services) {
1659 # do nothing (too many side effects)?
1660 # does not happen anyways, because config does not change.
1661 }
1662
1663 if (($self->rewrite_config_spam() && $restart_services) ||
1664 $force_restart->{spam}) {
1665 $log_restart->('pmg-smtp-filter');
1666 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1667 }
1668
1669 if (($self->rewrite_config_clam() && $restart_services) ||
1670 $force_restart->{clam}) {
1671 $log_restart->('clamav-daemon');
1672 PMG::Utils::service_cmd('clamav-daemon', 'restart');
1673 }
1674
1675 if (($self->rewrite_config_freshclam() && $restart_services) ||
1676 $force_restart->{freshclam}) {
1677 $log_restart->('clamav-freshclam');
1678 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
1679 }
1680
1681 if (($self->compare_smtp_filter_config() && $restart_services) ||
1682 $force_restart->{spam}) {
1683 syslog ('info', "scheduled reload for pmg-smtp-filter");
1684 PMG::Utils::reload_smtp_filter();
1685 }
1686 }
1687
1688 1;