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