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