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