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