]> git.proxmox.com Git - pmg-api.git/blob - PMG/Config.pm
fix #1679: do not delete old quarantine mails on cluster creation/join
[pmg-api.git] / 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 => "Secion 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 (/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 };
101 }
102
103 sub options {
104 return {
105 advfilter => { optional => 1 },
106 avast => { optional => 1 },
107 statlifetime => { optional => 1 },
108 dailyreport => { optional => 1 },
109 demo => { optional => 1 },
110 email => { optional => 1 },
111 http_proxy => { optional => 1 },
112 };
113 }
114
115 package PMG::Config::Spam;
116
117 use strict;
118 use warnings;
119
120 use base qw(PMG::Config::Base);
121
122 sub type {
123 return 'spam';
124 }
125
126 sub properties {
127 return {
128 languages => {
129 description => "This option is used to specify which languages are considered OK for incoming mail.",
130 type => 'string',
131 pattern => '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
132 default => 'all',
133 },
134 use_bayes => {
135 description => "Whether to use the naive-Bayesian-style classifier.",
136 type => 'boolean',
137 default => 1,
138 },
139 use_awl => {
140 description => "Use the Auto-Whitelist plugin.",
141 type => 'boolean',
142 default => 1,
143 },
144 use_razor => {
145 description => "Whether to use Razor2, if it is available.",
146 type => 'boolean',
147 default => 1,
148 },
149 wl_bounce_relays => {
150 description => "Whitelist legitimate bounce relays.",
151 type => 'string',
152 },
153 clamav_heuristic_score => {
154 description => "Score for ClamaAV heuristics (Google Safe Browsing database, PhishingScanURLs, ...).",
155 type => 'integer',
156 minimum => 0,
157 maximum => 1000,
158 default => 3,
159 },
160 bounce_score => {
161 description => "Additional score for bounce mails.",
162 type => 'integer',
163 minimum => 0,
164 maximum => 1000,
165 default => 0,
166 },
167 rbl_checks => {
168 description => "Enable real time blacklists (RBL) checks.",
169 type => 'boolean',
170 default => 1,
171 },
172 maxspamsize => {
173 description => "Maximum size of spam messages in bytes.",
174 type => 'integer',
175 minimum => 64,
176 default => 256*1024,
177 },
178 };
179 }
180
181 sub options {
182 return {
183 use_awl => { optional => 1 },
184 use_razor => { optional => 1 },
185 wl_bounce_relays => { optional => 1 },
186 languages => { optional => 1 },
187 use_bayes => { optional => 1 },
188 clamav_heuristic_score => { optional => 1 },
189 bounce_score => { optional => 1 },
190 rbl_checks => { optional => 1 },
191 maxspamsize => { optional => 1 },
192 };
193 }
194
195 package PMG::Config::SpamQuarantine;
196
197 use strict;
198 use warnings;
199
200 use base qw(PMG::Config::Base);
201
202 sub type {
203 return 'spamquar';
204 }
205
206 sub properties {
207 return {
208 lifetime => {
209 description => "Quarantine life time (days)",
210 type => 'integer',
211 minimum => 1,
212 default => 7,
213 },
214 authmode => {
215 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.",
216 type => 'string',
217 enum => [qw(ticket ldap ldapticket)],
218 default => 'ticket',
219 },
220 reportstyle => {
221 description => "Spam report style.",
222 type => 'string',
223 enum => [qw(none short verbose custom)],
224 default => 'verbose',
225 },
226 viewimages => {
227 description => "Allow to view images.",
228 type => 'boolean',
229 default => 1,
230 },
231 allowhrefs => {
232 description => "Allow to view hyperlinks.",
233 type => 'boolean',
234 default => 1,
235 },
236 hostname => {
237 description => "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
238 type => 'string', format => 'address',
239 },
240 port => {
241 description => "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
242 type => 'integer',
243 minimum => 1,
244 maximum => 65535,
245 default => 8006,
246 },
247 protocol => {
248 description => "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
249 type => 'string',
250 enum => [qw(http https)],
251 default => 'https',
252 },
253 mailfrom => {
254 description => "Text for 'From' header in daily spam report mails.",
255 type => 'string',
256 },
257 };
258 }
259
260 sub options {
261 return {
262 mailfrom => { optional => 1 },
263 hostname => { optional => 1 },
264 lifetime => { optional => 1 },
265 authmode => { optional => 1 },
266 reportstyle => { optional => 1 },
267 viewimages => { optional => 1 },
268 allowhrefs => { optional => 1 },
269 port => { optional => 1 },
270 protocol => { optional => 1 },
271 };
272 }
273
274 package PMG::Config::VirusQuarantine;
275
276 use strict;
277 use warnings;
278
279 use base qw(PMG::Config::Base);
280
281 sub type {
282 return 'virusquar';
283 }
284
285 sub properties {
286 return {};
287 }
288
289 sub options {
290 return {
291 lifetime => { optional => 1 },
292 viewimages => { optional => 1 },
293 allowhrefs => { optional => 1 },
294 };
295 }
296
297 package PMG::Config::ClamAV;
298
299 use strict;
300 use warnings;
301
302 use base qw(PMG::Config::Base);
303
304 sub type {
305 return 'clamav';
306 }
307
308 sub properties {
309 return {
310 dbmirror => {
311 description => "ClamAV database mirror server.",
312 type => 'string',
313 default => 'database.clamav.net',
314 },
315 archiveblockencrypted => {
316 description => "Wether to block encrypted archives. Mark encrypted archives as viruses.",
317 type => 'boolean',
318 default => 0,
319 },
320 archivemaxrec => {
321 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.",
322 type => 'integer',
323 minimum => 1,
324 default => 5,
325 },
326 archivemaxfiles => {
327 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.",
328 type => 'integer',
329 minimum => 0,
330 default => 1000,
331 },
332 archivemaxsize => {
333 description => "Files larger than this limit won't be scanned.",
334 type => 'integer',
335 minimum => 1000000,
336 default => 25000000,
337 },
338 maxscansize => {
339 description => "Sets the maximum amount of data to be scanned for each input file.",
340 type => 'integer',
341 minimum => 1000000,
342 default => 100000000,
343 },
344 maxcccount => {
345 description => "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
346 type => 'integer',
347 minimum => 0,
348 default => 0,
349 },
350 safebrowsing => {
351 description => "Enables support for Google Safe Browsing.",
352 type => 'boolean',
353 default => 1
354 },
355 };
356 }
357
358 sub options {
359 return {
360 archiveblockencrypted => { optional => 1 },
361 archivemaxrec => { optional => 1 },
362 archivemaxfiles => { optional => 1 },
363 archivemaxsize => { optional => 1 },
364 maxscansize => { optional => 1 },
365 dbmirror => { optional => 1 },
366 maxcccount => { optional => 1 },
367 safebrowsing => { optional => 1 },
368 };
369 }
370
371 package PMG::Config::Mail;
372
373 use strict;
374 use warnings;
375
376 use PVE::ProcFSTools;
377
378 use base qw(PMG::Config::Base);
379
380 sub type {
381 return 'mail';
382 }
383
384 my $physicalmem = 0;
385 sub physical_memory {
386
387 return $physicalmem if $physicalmem;
388
389 my $info = PVE::ProcFSTools::read_meminfo();
390 my $total = int($info->{memtotal} / (1024*1024));
391
392 return $total;
393 }
394
395 sub get_max_filters {
396 # estimate optimal number of filter servers
397
398 my $max_servers = 5;
399 my $servermem = 120;
400 my $memory = physical_memory();
401 my $add_servers = int(($memory - 512)/$servermem);
402 $max_servers += $add_servers if $add_servers > 0;
403 $max_servers = 40 if $max_servers > 40;
404
405 return $max_servers - 2;
406 }
407
408 sub get_max_smtpd {
409 # estimate optimal number of smtpd daemons
410
411 my $max_servers = 25;
412 my $servermem = 20;
413 my $memory = physical_memory();
414 my $add_servers = int(($memory - 512)/$servermem);
415 $max_servers += $add_servers if $add_servers > 0;
416 $max_servers = 100 if $max_servers > 100;
417 return $max_servers;
418 }
419
420 sub get_max_policy {
421 # estimate optimal number of proxpolicy servers
422 my $max_servers = 2;
423 my $memory = physical_memory();
424 $max_servers = 5 if $memory >= 500;
425 return $max_servers;
426 }
427
428 sub properties {
429 return {
430 int_port => {
431 description => "SMTP port number for outgoing mail (trusted).",
432 type => 'integer',
433 minimum => 1,
434 maximum => 65535,
435 default => 26,
436 },
437 ext_port => {
438 description => "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
439 type => 'integer',
440 minimum => 1,
441 maximum => 65535,
442 default => 25,
443 },
444 relay => {
445 description => "The default mail delivery transport (incoming mails).",
446 type => 'string', format => 'address',
447 },
448 relayport => {
449 description => "SMTP port number for relay host.",
450 type => 'integer',
451 minimum => 1,
452 maximum => 65535,
453 default => 25,
454 },
455 relaynomx => {
456 description => "Disable MX lookups for default relay.",
457 type => 'boolean',
458 default => 0,
459 },
460 smarthost => {
461 description => "When set, all outgoing mails are deliverd to the specified smarthost.",
462 type => 'string', format => 'address',
463 },
464 banner => {
465 description => "ESMTP banner.",
466 type => 'string',
467 maxLength => 1024,
468 default => 'ESMTP Proxmox',
469 },
470 max_filters => {
471 description => "Maximum number of pmg-smtp-filter processes.",
472 type => 'integer',
473 minimum => 3,
474 maximum => 40,
475 default => get_max_filters(),
476 },
477 max_policy => {
478 description => "Maximum number of pmgpolicy processes.",
479 type => 'integer',
480 minimum => 2,
481 maximum => 10,
482 default => get_max_policy(),
483 },
484 max_smtpd_in => {
485 description => "Maximum number of SMTP daemon processes (in).",
486 type => 'integer',
487 minimum => 3,
488 maximum => 100,
489 default => get_max_smtpd(),
490 },
491 max_smtpd_out => {
492 description => "Maximum number of SMTP daemon processes (out).",
493 type => 'integer',
494 minimum => 3,
495 maximum => 100,
496 default => get_max_smtpd(),
497 },
498 conn_count_limit => {
499 description => "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.",
500 type => 'integer',
501 minimum => 0,
502 default => 50,
503 },
504 conn_rate_limit => {
505 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.",
506 type => 'integer',
507 minimum => 0,
508 default => 0,
509 },
510 message_rate_limit => {
511 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.",
512 type => 'integer',
513 minimum => 0,
514 default => 0,
515 },
516 hide_received => {
517 description => "Hide received header in outgoing mails.",
518 type => 'boolean',
519 default => 0,
520 },
521 maxsize => {
522 description => "Maximum email size. Larger mails are rejected.",
523 type => 'integer',
524 minimum => 1024,
525 default => 1024*1024*10,
526 },
527 dwarning => {
528 description => "SMTP delay warning time (in hours).",
529 type => 'integer',
530 minimum => 0,
531 default => 4,
532 },
533 tls => {
534 description => "Enable TLS.",
535 type => 'boolean',
536 default => 0,
537 },
538 tlslog => {
539 description => "Enable TLS Logging.",
540 type => 'boolean',
541 default => 0,
542 },
543 tlsheader => {
544 description => "Add TLS received header.",
545 type => 'boolean',
546 default => 0,
547 },
548 spf => {
549 description => "Use Sender Policy Framework.",
550 type => 'boolean',
551 default => 1,
552 },
553 greylist => {
554 description => "Use Greylisting.",
555 type => 'boolean',
556 default => 1,
557 },
558 helotests => {
559 description => "Use SMTP HELO tests.",
560 type => 'boolean',
561 default => 0,
562 },
563 rejectunknown => {
564 description => "Reject unknown clients.",
565 type => 'boolean',
566 default => 0,
567 },
568 rejectunknownsender => {
569 description => "Reject unknown senders.",
570 type => 'boolean',
571 default => 0,
572 },
573 verifyreceivers => {
574 description => "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
575 type => 'string',
576 enum => ['450', '550'],
577 },
578 dnsbl_sites => {
579 description => "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
580 type => 'string', format => 'dnsbl-entry-list',
581 },
582 };
583 }
584
585 sub options {
586 return {
587 int_port => { optional => 1 },
588 ext_port => { optional => 1 },
589 smarthost => { optional => 1 },
590 relay => { optional => 1 },
591 relayport => { optional => 1 },
592 relaynomx => { optional => 1 },
593 dwarning => { optional => 1 },
594 max_smtpd_in => { optional => 1 },
595 max_smtpd_out => { optional => 1 },
596 greylist => { optional => 1 },
597 helotests => { optional => 1 },
598 tls => { optional => 1 },
599 tlslog => { optional => 1 },
600 tlsheader => { optional => 1 },
601 spf => { optional => 1 },
602 maxsize => { optional => 1 },
603 banner => { optional => 1 },
604 max_filters => { optional => 1 },
605 max_policy => { optional => 1 },
606 hide_received => { optional => 1 },
607 rejectunknown => { optional => 1 },
608 rejectunknownsender => { optional => 1 },
609 conn_count_limit => { optional => 1 },
610 conn_rate_limit => { optional => 1 },
611 message_rate_limit => { optional => 1 },
612 verifyreceivers => { optional => 1 },
613 dnsbl_sites => { optional => 1 },
614 };
615 }
616
617 package PMG::Config;
618
619 use strict;
620 use warnings;
621 use IO::File;
622 use Data::Dumper;
623 use Template;
624
625 use PVE::SafeSyslog;
626 use PVE::Tools qw($IPV4RE $IPV6RE);
627 use PVE::INotify;
628 use PVE::JSONSchema;
629
630 use PMG::Cluster;
631
632 PMG::Config::Admin->register();
633 PMG::Config::Mail->register();
634 PMG::Config::SpamQuarantine->register();
635 PMG::Config::VirusQuarantine->register();
636 PMG::Config::Spam->register();
637 PMG::Config::ClamAV->register();
638
639 # initialize all plugins
640 PMG::Config::Base->init();
641
642 PVE::JSONSchema::register_format(
643 'transport-domain', \&pmg_verify_transport_domain);
644
645 sub pmg_verify_transport_domain {
646 my ($name, $noerr) = @_;
647
648 # like dns-name, but can contain leading dot
649 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
650
651 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
652 return undef if $noerr;
653 die "value does not look like a valid transport domain\n";
654 }
655 return $name;
656 }
657
658 PVE::JSONSchema::register_format(
659 'transport-domain-or-email', \&pmg_verify_transport_domain_or_email);
660
661 sub pmg_verify_transport_domain_or_email {
662 my ($name, $noerr) = @_;
663
664 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
665
666 # email address
667 if ($name =~ m/^(?:[^\s\/\@]+\@)(${namere}\.)*${namere}$/) {
668 return $name;
669 }
670
671 # like dns-name, but can contain leading dot
672 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
673 return undef if $noerr;
674 die "value does not look like a valid transport domain or email address\n";
675 }
676 return $name;
677 }
678
679 PVE::JSONSchema::register_format(
680 'dnsbl-entry', \&pmg_verify_dnsbl_entry);
681
682 sub pmg_verify_dnsbl_entry {
683 my ($name, $noerr) = @_;
684
685 # like dns-name, but can contain trailing weight: 'domain*<WEIGHT>'
686 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
687
688 if ($name !~ /^(${namere}\.)*${namere}(\*\-?\d+)?$/) {
689 return undef if $noerr;
690 die "value '$name' does not look like a valid dnsbl entry\n";
691 }
692 return $name;
693 }
694
695 sub new {
696 my ($type) = @_;
697
698 my $class = ref($type) || $type;
699
700 my $cfg = PVE::INotify::read_file("pmg.conf");
701
702 return bless $cfg, $class;
703 }
704
705 sub write {
706 my ($self) = @_;
707
708 PVE::INotify::write_file("pmg.conf", $self);
709 }
710
711 my $lockfile = "/var/lock/pmgconfig.lck";
712
713 sub lock_config {
714 my ($code, $errmsg) = @_;
715
716 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
717 if (my $err = $@) {
718 $errmsg ? die "$errmsg: $err" : die $err;
719 }
720 }
721
722 # set section values
723 sub set {
724 my ($self, $section, $key, $value) = @_;
725
726 my $pdata = PMG::Config::Base->private();
727
728 my $plugin = $pdata->{plugins}->{$section};
729 die "no such section '$section'" if !$plugin;
730
731 if (defined($value)) {
732 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
733 $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
734 $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
735 } else {
736 if (defined($self->{ids}->{$section})) {
737 delete $self->{ids}->{$section}->{$key};
738 }
739 }
740
741 return undef;
742 }
743
744 # get section value or default
745 sub get {
746 my ($self, $section, $key, $nodefault) = @_;
747
748 my $pdata = PMG::Config::Base->private();
749 my $pdesc = $pdata->{propertyList}->{$key};
750 die "no such property '$section/$key'\n"
751 if !(defined($pdesc) && defined($pdata->{options}->{$section}) &&
752 defined($pdata->{options}->{$section}->{$key}));
753
754 if (defined($self->{ids}->{$section}) &&
755 defined(my $value = $self->{ids}->{$section}->{$key})) {
756 return $value;
757 }
758
759 return undef if $nodefault;
760
761 return $pdesc->{default};
762 }
763
764 # get a whole section with default value
765 sub get_section {
766 my ($self, $section) = @_;
767
768 my $pdata = PMG::Config::Base->private();
769 return undef if !defined($pdata->{options}->{$section});
770
771 my $res = {};
772
773 foreach my $key (keys %{$pdata->{options}->{$section}}) {
774
775 my $pdesc = $pdata->{propertyList}->{$key};
776
777 if (defined($self->{ids}->{$section}) &&
778 defined(my $value = $self->{ids}->{$section}->{$key})) {
779 $res->{$key} = $value;
780 next;
781 }
782 $res->{$key} = $pdesc->{default};
783 }
784
785 return $res;
786 }
787
788 # get a whole config with default values
789 sub get_config {
790 my ($self) = @_;
791
792 my $pdata = PMG::Config::Base->private();
793
794 my $res = {};
795
796 foreach my $type (keys %{$pdata->{plugins}}) {
797 my $plugin = $pdata->{plugins}->{$type};
798 $res->{$type} = $self->get_section($type);
799 }
800
801 return $res;
802 }
803
804 sub read_pmg_conf {
805 my ($filename, $fh) = @_;
806
807 local $/ = undef; # slurp mode
808
809 my $raw = <$fh> if defined($fh);
810
811 return PMG::Config::Base->parse_config($filename, $raw);
812 }
813
814 sub write_pmg_conf {
815 my ($filename, $fh, $cfg) = @_;
816
817 my $raw = PMG::Config::Base->write_config($filename, $cfg);
818
819 PVE::Tools::safe_print($filename, $fh, $raw);
820 }
821
822 PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
823 \&read_pmg_conf,
824 \&write_pmg_conf,
825 undef, always_call_parser => 1);
826
827 # parsers/writers for other files
828
829 my $domainsfilename = "/etc/pmg/domains";
830
831 sub postmap_pmg_domains {
832 PMG::Utils::run_postmap($domainsfilename);
833 }
834
835 sub read_pmg_domains {
836 my ($filename, $fh) = @_;
837
838 my $domains = {};
839
840 my $comment = '';
841 if (defined($fh)) {
842 while (defined(my $line = <$fh>)) {
843 chomp $line;
844 next if $line =~ m/^\s*$/;
845 if ($line =~ m/^#(.*)\s*$/) {
846 $comment = $1;
847 next;
848 }
849 if ($line =~ m/^(\S+)\s.*$/) {
850 my $domain = $1;
851 $domains->{$domain} = {
852 domain => $domain, comment => $comment };
853 $comment = '';
854 } else {
855 warn "parse error in '$filename': $line\n";
856 $comment = '';
857 }
858 }
859 }
860
861 return $domains;
862 }
863
864 sub write_pmg_domains {
865 my ($filename, $fh, $domains) = @_;
866
867 foreach my $domain (sort keys %$domains) {
868 my $comment = $domains->{$domain}->{comment};
869 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
870 if defined($comment) && $comment !~ m/^\s*$/;
871
872 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
873 }
874 }
875
876 PVE::INotify::register_file('domains', $domainsfilename,
877 \&read_pmg_domains,
878 \&write_pmg_domains,
879 undef, always_call_parser => 1);
880
881 my $mynetworks_filename = "/etc/pmg/mynetworks";
882
883 sub read_pmg_mynetworks {
884 my ($filename, $fh) = @_;
885
886 my $mynetworks = {};
887
888 my $comment = '';
889 if (defined($fh)) {
890 while (defined(my $line = <$fh>)) {
891 chomp $line;
892 next if $line =~ m/^\s*$/;
893 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
894 my ($network, $prefix_size, $comment) = ($1, $2, $3);
895 my $cidr = "$network/${prefix_size}";
896 $mynetworks->{$cidr} = {
897 cidr => $cidr,
898 network_address => $network,
899 prefix_size => $prefix_size,
900 comment => $comment // '',
901 };
902 } else {
903 warn "parse error in '$filename': $line\n";
904 }
905 }
906 }
907
908 return $mynetworks;
909 }
910
911 sub write_pmg_mynetworks {
912 my ($filename, $fh, $mynetworks) = @_;
913
914 foreach my $cidr (sort keys %$mynetworks) {
915 my $data = $mynetworks->{$cidr};
916 my $comment = $data->{comment} // '*';
917 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
918 }
919 }
920
921 PVE::INotify::register_file('mynetworks', $mynetworks_filename,
922 \&read_pmg_mynetworks,
923 \&write_pmg_mynetworks,
924 undef, always_call_parser => 1);
925
926 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
927
928 sub postmap_tls_policy {
929 PMG::Utils::run_postmap($tls_policy_map_filename);
930 }
931
932 my $transport_map_filename = "/etc/pmg/transport";
933
934 sub postmap_pmg_transport {
935 PMG::Utils::run_postmap($transport_map_filename);
936 }
937
938 sub read_transport_map {
939 my ($filename, $fh) = @_;
940
941 return [] if !defined($fh);
942
943 my $res = {};
944
945 my $comment = '';
946
947 while (defined(my $line = <$fh>)) {
948 chomp $line;
949 next if $line =~ m/^\s*$/;
950 if ($line =~ m/^#(.*)\s*$/) {
951 $comment = $1;
952 next;
953 }
954
955 my $parse_error = sub {
956 my ($err) = @_;
957 warn "parse error in '$filename': $line - $err";
958 $comment = '';
959 };
960
961 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
962 my ($domain, $host, $port) = ($1, $2, $3);
963
964 eval { pmg_verify_transport_domain_or_email($domain); };
965 if (my $err = $@) {
966 $parse_error->($err);
967 next;
968 }
969 my $use_mx = 1;
970 if ($host =~ m/^\[(.*)\]$/) {
971 $host = $1;
972 $use_mx = 0;
973 }
974
975 eval { PVE::JSONSchema::pve_verify_address($host); };
976 if (my $err = $@) {
977 $parse_error->($err);
978 next;
979 }
980
981 my $data = {
982 domain => $domain,
983 host => $host,
984 port => $port,
985 use_mx => $use_mx,
986 comment => $comment,
987 };
988 $res->{$domain} = $data;
989 $comment = '';
990 } else {
991 $parse_error->('wrong format');
992 }
993 }
994
995 return $res;
996 }
997
998 sub write_transport_map {
999 my ($filename, $fh, $tmap) = @_;
1000
1001 return if !$tmap;
1002
1003 foreach my $domain (sort keys %$tmap) {
1004 my $data = $tmap->{$domain};
1005
1006 my $comment = $data->{comment};
1007 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1008 if defined($comment) && $comment !~ m/^\s*$/;
1009
1010 my $use_mx = $data->{use_mx};
1011 $use_mx = 0 if $data->{host} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1012
1013 if ($use_mx) {
1014 PVE::Tools::safe_print(
1015 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1016 } else {
1017 PVE::Tools::safe_print(
1018 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1019 }
1020 }
1021 }
1022
1023 PVE::INotify::register_file('transport', $transport_map_filename,
1024 \&read_transport_map,
1025 \&write_transport_map,
1026 undef, always_call_parser => 1);
1027
1028 # config file generation using templates
1029
1030 sub get_template_vars {
1031 my ($self) = @_;
1032
1033 my $vars = { pmg => $self->get_config() };
1034
1035 my $nodename = PVE::INotify::nodename();
1036 my $int_ip = PMG::Cluster::remote_node_ip($nodename);
1037 $vars->{ipconfig}->{int_ip} = $int_ip;
1038
1039 my $transportnets = [];
1040
1041 if (my $tmap = PVE::INotify::read_file('transport')) {
1042 foreach my $domain (sort keys %$tmap) {
1043 my $data = $tmap->{$domain};
1044 my $host = $data->{host};
1045 if ($host =~ m/^$IPV4RE$/) {
1046 push @$transportnets, "$host/32";
1047 } elsif ($host =~ m/^$IPV6RE$/) {
1048 push @$transportnets, "[$host]/128";
1049 }
1050 }
1051 }
1052
1053 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
1054
1055 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1056
1057 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
1058 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1059 push @$mynetworks, "[$1]/$2";
1060 } else {
1061 push @$mynetworks, $int_net_cidr;
1062 }
1063 } else {
1064 if ($int_ip =~ m/^$IPV6RE$/) {
1065 push @$mynetworks, "[$int_ip]/128";
1066 } else {
1067 push @$mynetworks, "$int_ip/32";
1068 }
1069 }
1070
1071 my $netlist = PVE::INotify::read_file('mynetworks');
1072 foreach my $cidr (keys %$netlist) {
1073 if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1074 push @$mynetworks, "[$1]/$2";
1075 } else {
1076 push @$mynetworks, $cidr;
1077 }
1078 }
1079
1080 push @$mynetworks, @$transportnets;
1081
1082 # add default relay to mynetworks
1083 if (my $relay = $self->get('mail', 'relay')) {
1084 if ($relay =~ m/^$IPV4RE$/) {
1085 push @$mynetworks, "$relay/32";
1086 } elsif ($relay =~ m/^$IPV6RE$/) {
1087 push @$mynetworks, "[$relay]/128";
1088 } else {
1089 # DNS name - do nothing ?
1090 }
1091 }
1092
1093 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1094
1095 # normalize dnsbl_sites
1096 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1097 if (scalar(@dnsbl_sites)) {
1098 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1099 }
1100
1101 my $usepolicy = 0;
1102 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1103 $self->get('mail', 'spf');
1104 $vars->{postfix}->{usepolicy} = $usepolicy;
1105
1106 if ($int_ip =~ m/^$IPV6RE$/) {
1107 $vars->{postfix}->{int_ip} = "[$int_ip]";
1108 } else {
1109 $vars->{postfix}->{int_ip} = $int_ip;
1110 }
1111
1112 my $resolv = PVE::INotify::read_file('resolvconf');
1113 $vars->{dns}->{hostname} = $nodename;
1114
1115 my $domain = $resolv->{search} // 'localdomain';
1116 $vars->{dns}->{domain} = $domain;
1117
1118 my $wlbr = "$nodename.$domain";
1119 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1120 $wlbr .= " $r"
1121 }
1122 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1123
1124 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1125 eval {
1126 my $uri = URI->new($proxy);
1127 my $host = $uri->host;
1128 my $port = $uri->port // 8080;
1129 if ($host) {
1130 my $data = { host => $host, port => $port };
1131 if (my $ui = $uri->userinfo) {
1132 my ($username, $pw) = split(/:/, $ui, 2);
1133 $data->{username} = $username;
1134 $data->{password} = $pw if defined($pw);
1135 }
1136 $vars->{proxy} = $data;
1137 }
1138 };
1139 warn "parse http_proxy failed - $@" if $@;
1140 }
1141
1142 return $vars;
1143 }
1144
1145 # use one global TT cache
1146 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1147
1148 my $template_toolkit;
1149
1150 sub get_template_toolkit {
1151
1152 return $template_toolkit if $template_toolkit;
1153
1154 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1155
1156 return $template_toolkit;
1157 }
1158
1159 # rewrite file from template
1160 # return true if file has changed
1161 sub rewrite_config_file {
1162 my ($self, $tmplname, $dstfn) = @_;
1163
1164 my $demo = $self->get('admin', 'demo');
1165
1166 if ($demo) {
1167 my $demosrc = "$tmplname.demo";
1168 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
1169 }
1170
1171 my ($perm, $uid, $gid);
1172
1173 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1174 # needed if file contains a HTTPProxyPasswort
1175
1176 $uid = getpwnam('clamav');
1177 $gid = getgrnam('adm');
1178 $perm = 0600;
1179 }
1180
1181 my $tt = get_template_toolkit();
1182
1183 my $vars = $self->get_template_vars();
1184
1185 my $output = '';
1186
1187 $tt->process($tmplname, $vars, \$output) ||
1188 die $tt->error() . "\n";
1189
1190 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1191
1192 return 0 if defined($old) && ($old eq $output); # no change
1193
1194 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1195
1196 if (defined($uid) && defined($gid)) {
1197 chown($uid, $gid, $dstfn);
1198 }
1199
1200 return 1;
1201 }
1202
1203 # rewrite spam configuration
1204 sub rewrite_config_spam {
1205 my ($self) = @_;
1206
1207 my $use_awl = $self->get('spam', 'use_awl');
1208 my $use_bayes = $self->get('spam', 'use_bayes');
1209 my $use_razor = $self->get('spam', 'use_razor');
1210
1211 my $changes = 0;
1212
1213 # delete AW and bayes databases if those features are disabled
1214 if (!$use_awl) {
1215 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1216 }
1217
1218 if (!$use_bayes) {
1219 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1220 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1221 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1222 }
1223
1224 # make sure we have a custom.cf file (else cluster sync fails)
1225 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1226
1227 $changes = 1 if $self->rewrite_config_file(
1228 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1229
1230 $changes = 1 if $self->rewrite_config_file(
1231 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1232
1233 $changes = 1 if $self->rewrite_config_file(
1234 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1235
1236 $changes = 1 if $self->rewrite_config_file(
1237 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1238
1239 if ($use_razor) {
1240 mkdir "/root/.razor";
1241
1242 $changes = 1 if $self->rewrite_config_file(
1243 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1244
1245 if (! -e '/root/.razor/identity') {
1246 eval {
1247 my $timeout = 30;
1248 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1249 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
1250 };
1251 my $err = $@;
1252 syslog('info', "registering razor failed: $err") if $err;
1253 }
1254 }
1255
1256 return $changes;
1257 }
1258
1259 # rewrite ClamAV configuration
1260 sub rewrite_config_clam {
1261 my ($self) = @_;
1262
1263 return $self->rewrite_config_file(
1264 'clamd.conf.in', '/etc/clamav/clamd.conf');
1265 }
1266
1267 sub rewrite_config_freshclam {
1268 my ($self) = @_;
1269
1270 return $self->rewrite_config_file(
1271 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1272 }
1273
1274 sub rewrite_config_postgres {
1275 my ($self) = @_;
1276
1277 my $pgconfdir = "/etc/postgresql/9.6/main";
1278
1279 my $changes = 0;
1280
1281 $changes = 1 if $self->rewrite_config_file(
1282 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1283
1284 $changes = 1 if $self->rewrite_config_file(
1285 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1286
1287 return $changes;
1288 }
1289
1290 # rewrite /root/.forward
1291 sub rewrite_dot_forward {
1292 my ($self) = @_;
1293
1294 my $dstfn = '/root/.forward';
1295
1296 my $email = $self->get('admin', 'email');
1297
1298 my $output = '';
1299 if ($email && $email =~ m/\s*(\S+)\s*/) {
1300 $output = "$1\n";
1301 } else {
1302 # empty .forward does not forward mails (see man local)
1303 }
1304
1305 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1306
1307 return 0 if defined($old) && ($old eq $output); # no change
1308
1309 PVE::Tools::file_set_contents($dstfn, $output);
1310
1311 return 1;
1312 }
1313
1314 my $write_smtp_whitelist = sub {
1315 my ($filename, $data, $action) = @_;
1316
1317 $action = 'OK' if !$action;
1318
1319 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1320 if -f $filename;
1321
1322 my $new = '';
1323 foreach my $k (sort keys %$data) {
1324 $new .= "$k $action\n";
1325 }
1326
1327 return 0 if defined($old) && ($old eq $new); # no change
1328
1329 PVE::Tools::file_set_contents($filename, $new);
1330
1331 PMG::Utils::run_postmap($filename);
1332
1333 return 1;
1334 };
1335
1336 sub rewrite_postfix_whitelist {
1337 my ($rulecache) = @_;
1338
1339 # see man page for regexp_table for postfix regex table format
1340
1341 # we use a hash to avoid duplicate entries in regex tables
1342 my $tolist = {};
1343 my $fromlist = {};
1344 my $clientlist = {};
1345
1346 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1347 my $oclass = ref($obj);
1348 if ($oclass eq 'PMG::RuleDB::Receiver') {
1349 my $addr = PMG::Utils::quote_regex($obj->{address});
1350 $tolist->{"/^$addr\$/"} = 1;
1351 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1352 my $addr = PMG::Utils::quote_regex($obj->{address});
1353 $tolist->{"/^.+\@$addr\$/"} = 1;
1354 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1355 my $addr = $obj->{address};
1356 $addr =~ s|/|\\/|g;
1357 $tolist->{"/^$addr\$/"} = 1;
1358 }
1359 }
1360
1361 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1362 my $oclass = ref($obj);
1363 my $addr = PMG::Utils::quote_regex($obj->{address});
1364 if ($oclass eq 'PMG::RuleDB::EMail') {
1365 my $addr = PMG::Utils::quote_regex($obj->{address});
1366 $fromlist->{"/^$addr\$/"} = 1;
1367 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1368 my $addr = PMG::Utils::quote_regex($obj->{address});
1369 $fromlist->{"/^.+\@$addr\$/"} = 1;
1370 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1371 my $addr = $obj->{address};
1372 $addr =~ s|/|\\/|g;
1373 $fromlist->{"/^$addr\$/"} = 1;
1374 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1375 $clientlist->{$obj->{address}} = 1;
1376 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1377 $clientlist->{$obj->{address}} = 1;
1378 }
1379 }
1380
1381 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1382 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1383 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1384 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1385 };
1386
1387 # rewrite /etc/postfix/*
1388 sub rewrite_config_postfix {
1389 my ($self, $rulecache) = @_;
1390
1391 # make sure we have required files (else postfix start fails)
1392 IO::File->new($transport_map_filename, 'a', 0644);
1393
1394 my $changes = 0;
1395
1396 if ($self->get('mail', 'tls')) {
1397 eval {
1398 PMG::Utils::gen_proxmox_tls_cert();
1399 };
1400 syslog ('info', "generating certificate failed: $@") if $@;
1401 }
1402
1403 $changes = 1 if $self->rewrite_config_file(
1404 'main.cf.in', '/etc/postfix/main.cf');
1405
1406 $changes = 1 if $self->rewrite_config_file(
1407 'master.cf.in', '/etc/postfix/master.cf');
1408
1409 # make sure we have required files (else postfix start fails)
1410 # Note: postmap need a valid /etc/postfix/main.cf configuration
1411 postmap_pmg_domains();
1412 postmap_pmg_transport();
1413 postmap_tls_policy();
1414
1415 rewrite_postfix_whitelist($rulecache) if $rulecache;
1416
1417 # make sure aliases.db is up to date
1418 system('/usr/bin/newaliases');
1419
1420 return $changes;
1421 }
1422
1423 sub rewrite_config {
1424 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1425
1426 $force_restart = {} if ! $force_restart;
1427
1428 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1429 $force_restart->{postfix}) {
1430 PMG::Utils::service_cmd('postfix', 'restart');
1431 }
1432
1433 if ($self->rewrite_dot_forward() && $restart_services) {
1434 # no need to restart anything
1435 }
1436
1437 if ($self->rewrite_config_postgres() && $restart_services) {
1438 # do nothing (too many side effects)?
1439 # does not happen anyways, because config does not change.
1440 }
1441
1442 if (($self->rewrite_config_spam() && $restart_services) ||
1443 $force_restart->{spam}) {
1444 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1445 }
1446
1447 if (($self->rewrite_config_clam() && $restart_services) ||
1448 $force_restart->{clam}) {
1449 PMG::Utils::service_cmd('clamav-daemon', 'restart');
1450 }
1451
1452 if (($self->rewrite_config_freshclam() && $restart_services) ||
1453 $force_restart->{freshclam}) {
1454 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
1455 }
1456 }
1457
1458 1;