]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/Config.pm
allow for optional 'ipv6:' prefix in transports
[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 PVE::JSONSchema::register_format(
1106 'transport-address', \&pmg_verify_transport_address);
1107
1108 sub pmg_verify_transport_address {
1109 my ($name, $noerr) = @_;
1110
1111 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1112 return $name;
1113 } elsif (PVE::JSONSchema::pve_verify_address($name, 1)) {
1114 return $name;
1115 } else {
1116 return undef if $noerr;
1117 die "value does not look like a valid address\n";
1118 }
1119 }
1120
1121 sub read_transport_map {
1122 my ($filename, $fh) = @_;
1123
1124 return [] if !defined($fh);
1125
1126 my $res = {};
1127
1128 my $comment = '';
1129
1130 while (defined(my $line = <$fh>)) {
1131 chomp $line;
1132 next if $line =~ m/^\s*$/;
1133 if ($line =~ m/^#(.*)\s*$/) {
1134 $comment = $1;
1135 next;
1136 }
1137
1138 my $parse_error = sub {
1139 my ($err) = @_;
1140 warn "parse error in '$filename': $line - $err";
1141 $comment = '';
1142 };
1143
1144 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1145 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1146
1147 eval { pmg_verify_transport_domain_or_email($domain); };
1148 if (my $err = $@) {
1149 $parse_error->($err);
1150 next;
1151 }
1152 my $use_mx = 1;
1153 if ($host =~ m/^\[(.*)\]$/) {
1154 $host = $1;
1155 $use_mx = 0;
1156 }
1157 $use_mx = 0 if ($protocol eq "lmtp");
1158
1159 eval { pmg_verify_transport_address($host); };
1160 if (my $err = $@) {
1161 $parse_error->($err);
1162 next;
1163 }
1164
1165 my $data = {
1166 domain => $domain,
1167 protocol => $protocol,
1168 host => $host,
1169 port => $port,
1170 use_mx => $use_mx,
1171 comment => $comment,
1172 };
1173 $res->{$domain} = $data;
1174 $comment = '';
1175 } else {
1176 $parse_error->('wrong format');
1177 }
1178 }
1179
1180 return $res;
1181 }
1182
1183 sub write_transport_map {
1184 my ($filename, $fh, $tmap) = @_;
1185
1186 return if !$tmap;
1187
1188 foreach my $domain (sort keys %$tmap) {
1189 my $data = $tmap->{$domain};
1190
1191 my $comment = $data->{comment};
1192 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1193 if defined($comment) && $comment !~ m/^\s*$/;
1194
1195 my $bracket_host = !$data->{use_mx};
1196
1197 if ($data->{protocol} eq 'lmtp') {
1198 $bracket_host = 0;
1199 $data->{protocol} .= ":inet";
1200 }
1201 $bracket_host = 1 if $data->{host} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1202 my $host = $bracket_host ? "[$data->{host}]" : $data->{host};
1203
1204 PVE::Tools::safe_print($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1205 }
1206 }
1207
1208 PVE::INotify::register_file('transport', $transport_map_filename,
1209 \&read_transport_map,
1210 \&write_transport_map,
1211 undef, always_call_parser => 1);
1212
1213 # config file generation using templates
1214
1215 sub get_host_dns_info {
1216 my ($self) = @_;
1217
1218 my $dnsinfo = {};
1219 my $nodename = PVE::INotify::nodename();
1220
1221 $dnsinfo->{hostname} = $nodename;
1222 my $resolv = PVE::INotify::read_file('resolvconf');
1223
1224 my $domain = $resolv->{search} // 'localdomain';
1225 $dnsinfo->{domain} = $domain;
1226
1227 $dnsinfo->{fqdn} = "$nodename.$domain";
1228
1229 return $dnsinfo;
1230 }
1231
1232 sub get_template_vars {
1233 my ($self) = @_;
1234
1235 my $vars = { pmg => $self->get_config() };
1236
1237 my $dnsinfo = get_host_dns_info();
1238 $vars->{dns} = $dnsinfo;
1239 my $int_ip = PMG::Cluster::remote_node_ip($dnsinfo->{hostname});
1240 $vars->{ipconfig}->{int_ip} = $int_ip;
1241
1242 my $transportnets = [];
1243
1244 if (my $tmap = PVE::INotify::read_file('transport')) {
1245 foreach my $domain (sort keys %$tmap) {
1246 my $data = $tmap->{$domain};
1247 my $host = $data->{host};
1248 if ($host =~ m/^$IPV4RE$/) {
1249 push @$transportnets, "$host/32";
1250 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1251 push @$transportnets, "[$1]/128";
1252 }
1253 }
1254 }
1255
1256 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
1257
1258 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1259
1260 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
1261 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1262 push @$mynetworks, "[$1]/$2";
1263 } else {
1264 push @$mynetworks, $int_net_cidr;
1265 }
1266 } else {
1267 if ($int_ip =~ m/^$IPV6RE$/) {
1268 push @$mynetworks, "[$int_ip]/128";
1269 } else {
1270 push @$mynetworks, "$int_ip/32";
1271 }
1272 }
1273
1274 my $netlist = PVE::INotify::read_file('mynetworks');
1275 foreach my $cidr (sort keys %$netlist) {
1276 if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1277 push @$mynetworks, "[$1]/$2";
1278 } else {
1279 push @$mynetworks, $cidr;
1280 }
1281 }
1282
1283 push @$mynetworks, @$transportnets;
1284
1285 # add default relay to mynetworks
1286 if (my $relay = $self->get('mail', 'relay')) {
1287 if ($relay =~ m/^$IPV4RE$/) {
1288 push @$mynetworks, "$relay/32";
1289 } elsif ($relay =~ m/^$IPV6RE$/) {
1290 push @$mynetworks, "[$relay]/128";
1291 } else {
1292 # DNS name - do nothing ?
1293 }
1294 }
1295
1296 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1297
1298 # normalize dnsbl_sites
1299 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1300 if (scalar(@dnsbl_sites)) {
1301 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1302 }
1303
1304 $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
1305
1306 my $usepolicy = 0;
1307 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1308 $self->get('mail', 'spf');
1309 $vars->{postfix}->{usepolicy} = $usepolicy;
1310
1311 if ($int_ip =~ m/^$IPV6RE$/) {
1312 $vars->{postfix}->{int_ip} = "[$int_ip]";
1313 } else {
1314 $vars->{postfix}->{int_ip} = $int_ip;
1315 }
1316
1317 my $wlbr = $dnsinfo->{fqdn};
1318 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1319 $wlbr .= " $r"
1320 }
1321 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1322
1323 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1324 eval {
1325 my $uri = URI->new($proxy);
1326 my $host = $uri->host;
1327 my $port = $uri->port // 8080;
1328 if ($host) {
1329 my $data = { host => $host, port => $port };
1330 if (my $ui = $uri->userinfo) {
1331 my ($username, $pw) = split(/:/, $ui, 2);
1332 $data->{username} = $username;
1333 $data->{password} = $pw if defined($pw);
1334 }
1335 $vars->{proxy} = $data;
1336 }
1337 };
1338 warn "parse http_proxy failed - $@" if $@;
1339 }
1340 $vars->{postgres}->{version} = PMG::Utils::get_pg_server_version();
1341
1342 return $vars;
1343 }
1344
1345 # use one global TT cache
1346 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1347
1348 my $template_toolkit;
1349
1350 sub get_template_toolkit {
1351
1352 return $template_toolkit if $template_toolkit;
1353
1354 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1355
1356 return $template_toolkit;
1357 }
1358
1359 # rewrite file from template
1360 # return true if file has changed
1361 sub rewrite_config_file {
1362 my ($self, $tmplname, $dstfn) = @_;
1363
1364 my $demo = $self->get('admin', 'demo');
1365
1366 if ($demo) {
1367 my $demosrc = "$tmplname.demo";
1368 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
1369 }
1370
1371 my ($perm, $uid, $gid);
1372
1373 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1374 # needed if file contains a HTTPProxyPasswort
1375
1376 $uid = getpwnam('clamav');
1377 $gid = getgrnam('adm');
1378 $perm = 0600;
1379 }
1380
1381 my $tt = get_template_toolkit();
1382
1383 my $vars = $self->get_template_vars();
1384
1385 my $output = '';
1386
1387 $tt->process($tmplname, $vars, \$output) ||
1388 die $tt->error() . "\n";
1389
1390 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1391
1392 return 0 if defined($old) && ($old eq $output); # no change
1393
1394 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1395
1396 if (defined($uid) && defined($gid)) {
1397 chown($uid, $gid, $dstfn);
1398 }
1399
1400 return 1;
1401 }
1402
1403 # rewrite spam configuration
1404 sub rewrite_config_spam {
1405 my ($self) = @_;
1406
1407 my $use_awl = $self->get('spam', 'use_awl');
1408 my $use_bayes = $self->get('spam', 'use_bayes');
1409 my $use_razor = $self->get('spam', 'use_razor');
1410
1411 my $changes = 0;
1412
1413 # delete AW and bayes databases if those features are disabled
1414 if (!$use_awl) {
1415 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1416 }
1417
1418 if (!$use_bayes) {
1419 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1420 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1421 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1422 }
1423
1424 # make sure we have the custom SA files (else cluster sync fails)
1425 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1426 IO::File->new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1427
1428 $changes = 1 if $self->rewrite_config_file(
1429 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1430
1431 $changes = 1 if $self->rewrite_config_file(
1432 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1433
1434 $changes = 1 if $self->rewrite_config_file(
1435 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1436
1437 $changes = 1 if $self->rewrite_config_file(
1438 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1439
1440 if ($use_razor) {
1441 mkdir "/root/.razor";
1442
1443 $changes = 1 if $self->rewrite_config_file(
1444 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1445
1446 if (! -e '/root/.razor/identity') {
1447 eval {
1448 my $timeout = 30;
1449 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1450 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
1451 };
1452 my $err = $@;
1453 syslog('info', "registering razor failed: $err") if $err;
1454 }
1455 }
1456
1457 return $changes;
1458 }
1459
1460 # rewrite ClamAV configuration
1461 sub rewrite_config_clam {
1462 my ($self) = @_;
1463
1464 return $self->rewrite_config_file(
1465 'clamd.conf.in', '/etc/clamav/clamd.conf');
1466 }
1467
1468 sub rewrite_config_freshclam {
1469 my ($self) = @_;
1470
1471 return $self->rewrite_config_file(
1472 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1473 }
1474
1475 sub rewrite_config_postgres {
1476 my ($self) = @_;
1477
1478 my $pg_maj_version = PMG::Utils::get_pg_server_version();
1479 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1480
1481 my $changes = 0;
1482
1483 $changes = 1 if $self->rewrite_config_file(
1484 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1485
1486 $changes = 1 if $self->rewrite_config_file(
1487 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1488
1489 return $changes;
1490 }
1491
1492 # rewrite /root/.forward
1493 sub rewrite_dot_forward {
1494 my ($self) = @_;
1495
1496 my $dstfn = '/root/.forward';
1497
1498 my $email = $self->get('admin', 'email');
1499
1500 my $output = '';
1501 if ($email && $email =~ m/\s*(\S+)\s*/) {
1502 $output = "$1\n";
1503 } else {
1504 # empty .forward does not forward mails (see man local)
1505 }
1506
1507 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1508
1509 return 0 if defined($old) && ($old eq $output); # no change
1510
1511 PVE::Tools::file_set_contents($dstfn, $output);
1512
1513 return 1;
1514 }
1515
1516 my $write_smtp_whitelist = sub {
1517 my ($filename, $data, $action) = @_;
1518
1519 $action = 'OK' if !$action;
1520
1521 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1522 if -f $filename;
1523
1524 my $new = '';
1525 foreach my $k (sort keys %$data) {
1526 $new .= "$k $action\n";
1527 }
1528
1529 return 0 if defined($old) && ($old eq $new); # no change
1530
1531 PVE::Tools::file_set_contents($filename, $new);
1532
1533 PMG::Utils::run_postmap($filename);
1534
1535 return 1;
1536 };
1537
1538 sub rewrite_postfix_whitelist {
1539 my ($rulecache) = @_;
1540
1541 # see man page for regexp_table for postfix regex table format
1542
1543 # we use a hash to avoid duplicate entries in regex tables
1544 my $tolist = {};
1545 my $fromlist = {};
1546 my $clientlist = {};
1547
1548 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1549 my $oclass = ref($obj);
1550 if ($oclass eq 'PMG::RuleDB::Receiver') {
1551 my $addr = PMG::Utils::quote_regex($obj->{address});
1552 $tolist->{"/^$addr\$/"} = 1;
1553 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1554 my $addr = PMG::Utils::quote_regex($obj->{address});
1555 $tolist->{"/^.+\@$addr\$/"} = 1;
1556 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1557 my $addr = $obj->{address};
1558 $addr =~ s|/|\\/|g;
1559 $tolist->{"/^$addr\$/"} = 1;
1560 }
1561 }
1562
1563 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1564 my $oclass = ref($obj);
1565 my $addr = PMG::Utils::quote_regex($obj->{address});
1566 if ($oclass eq 'PMG::RuleDB::EMail') {
1567 my $addr = PMG::Utils::quote_regex($obj->{address});
1568 $fromlist->{"/^$addr\$/"} = 1;
1569 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1570 my $addr = PMG::Utils::quote_regex($obj->{address});
1571 $fromlist->{"/^.+\@$addr\$/"} = 1;
1572 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1573 my $addr = $obj->{address};
1574 $addr =~ s|/|\\/|g;
1575 $fromlist->{"/^$addr\$/"} = 1;
1576 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1577 $clientlist->{$obj->{address}} = 1;
1578 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1579 $clientlist->{$obj->{address}} = 1;
1580 }
1581 }
1582
1583 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1584 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1585 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1586 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1587 };
1588
1589 # rewrite /etc/postfix/*
1590 sub rewrite_config_postfix {
1591 my ($self, $rulecache) = @_;
1592
1593 # make sure we have required files (else postfix start fails)
1594 IO::File->new($transport_map_filename, 'a', 0644);
1595
1596 my $changes = 0;
1597
1598 if ($self->get('mail', 'tls')) {
1599 eval {
1600 PMG::Utils::gen_proxmox_tls_cert();
1601 };
1602 syslog ('info', "generating certificate failed: $@") if $@;
1603 }
1604
1605 $changes = 1 if $self->rewrite_config_file(
1606 'main.cf.in', '/etc/postfix/main.cf');
1607
1608 $changes = 1 if $self->rewrite_config_file(
1609 'master.cf.in', '/etc/postfix/master.cf');
1610
1611 # make sure we have required files (else postfix start fails)
1612 # Note: postmap need a valid /etc/postfix/main.cf configuration
1613 postmap_pmg_domains();
1614 postmap_pmg_transport();
1615 postmap_tls_policy();
1616
1617 rewrite_postfix_whitelist($rulecache) if $rulecache;
1618
1619 # make sure aliases.db is up to date
1620 system('/usr/bin/newaliases');
1621
1622 return $changes;
1623 }
1624
1625 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1626 my $pmg_service_params = {
1627 mail => {
1628 hide_received => 1,
1629 ndr_on_block => 1,
1630 },
1631 admin => {
1632 dkim_selector => 1,
1633 dkim_sign => 1,
1634 dkim_sign_all_mail => 1,
1635 },
1636 };
1637
1638 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1639 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1640
1641 sub dump_smtp_filter_config {
1642 my ($self) = @_;
1643
1644 my $conf = '';
1645 my $val;
1646 foreach my $sec (sort keys %$pmg_service_params) {
1647 my $conf_sec = $self->{ids}->{$sec} // {};
1648 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1649 $val = $conf_sec->{$key};
1650 $conf .= "$sec.$key:$val\n" if defined($val);
1651 }
1652 }
1653
1654 return $conf;
1655 }
1656
1657 sub compare_smtp_filter_config {
1658 my ($self) = @_;
1659
1660 my $ret = 0;
1661 my $old;
1662 eval {
1663 $old = PVE::Tools::file_get_contents($smtp_filter_cfg);
1664 };
1665
1666 if (my $err = $@) {
1667 syslog ('warning', "reloading pmg-smtp-filter: $err");
1668 $ret = 1;
1669 } else {
1670 my $new = $self->dump_smtp_filter_config();
1671 $ret = 1 if $old ne $new;
1672 }
1673
1674 $self->write_smtp_filter_config() if $ret;
1675
1676 return $ret;
1677 }
1678
1679 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1680 # on config change
1681 sub write_smtp_filter_config {
1682 my ($self) = @_;
1683
1684 PVE::Tools::lock_file($smtp_filter_cfg_lock, undef, sub {
1685 PVE::Tools::file_set_contents($smtp_filter_cfg,
1686 $self->dump_smtp_filter_config());
1687 });
1688
1689 die $@ if $@;
1690 }
1691
1692 sub rewrite_config {
1693 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1694
1695 $force_restart = {} if ! $force_restart;
1696
1697 my $log_restart = sub {
1698 syslog ('info', "configuration change detected for '$_[0]', restarting");
1699 };
1700
1701 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1702 $force_restart->{postfix}) {
1703 $log_restart->('postfix');
1704 PMG::Utils::service_cmd('postfix', 'reload');
1705 }
1706
1707 if ($self->rewrite_dot_forward() && $restart_services) {
1708 # no need to restart anything
1709 }
1710
1711 if ($self->rewrite_config_postgres() && $restart_services) {
1712 # do nothing (too many side effects)?
1713 # does not happen anyways, because config does not change.
1714 }
1715
1716 if (($self->rewrite_config_spam() && $restart_services) ||
1717 $force_restart->{spam}) {
1718 $log_restart->('pmg-smtp-filter');
1719 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1720 }
1721
1722 if (($self->rewrite_config_clam() && $restart_services) ||
1723 $force_restart->{clam}) {
1724 $log_restart->('clamav-daemon');
1725 PMG::Utils::service_cmd('clamav-daemon', 'restart');
1726 }
1727
1728 if (($self->rewrite_config_freshclam() && $restart_services) ||
1729 $force_restart->{freshclam}) {
1730 $log_restart->('clamav-freshclam');
1731 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
1732 }
1733
1734 if (($self->compare_smtp_filter_config() && $restart_services) ||
1735 $force_restart->{spam}) {
1736 syslog ('info', "scheduled reload for pmg-smtp-filter");
1737 PMG::Utils::reload_smtp_filter();
1738 }
1739 }
1740
1741 1;