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