]> git.proxmox.com Git - pmg-api.git/blob - PMG/Config.pm
956963997a87449683756e8b4c4bcb4da85797bd
[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 => 256*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 => 26,
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 => 25,
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 $vars->{ipconfig}->{int_ip} = $int_ip;
996
997 my $transportnets = [];
998
999 if (my $tmap = PVE::INotify::read_file('transport')) {
1000 foreach my $domain (sort keys %$tmap) {
1001 my $data = $tmap->{$domain};
1002 my $host = $data->{host};
1003 if ($host =~ m/^$IPV4RE$/) {
1004 push @$transportnets, "$host/32";
1005 } elsif ($host =~ m/^$IPV6RE$/) {
1006 push @$transportnets, "[$host]/128";
1007 }
1008 }
1009 }
1010
1011 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
1012
1013 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1014
1015 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
1016 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1017 push @$mynetworks, "[$1]/$2";
1018 } else {
1019 push @$mynetworks, $int_net_cidr;
1020 }
1021 } else {
1022 if ($int_ip =~ m/^$IPV6RE$/) {
1023 push @$mynetworks, "[$int_ip]/128";
1024 } else {
1025 push @$mynetworks, "$int_ip/32";
1026 }
1027 }
1028
1029 my $netlist = PVE::INotify::read_file('mynetworks');
1030 foreach my $cidr (keys %$netlist) {
1031 if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1032 push @$mynetworks, "[$1]/$2";
1033 } else {
1034 push @$mynetworks, $cidr;
1035 }
1036 }
1037
1038 push @$mynetworks, @$transportnets;
1039
1040 # add default relay to mynetworks
1041 if (my $relay = $self->get('mail', 'relay')) {
1042 if ($relay =~ m/^$IPV4RE$/) {
1043 push @$mynetworks, "$relay/32";
1044 } elsif ($relay =~ m/^$IPV6RE$/) {
1045 push @$mynetworks, "[$relay]/128";
1046 } else {
1047 # DNS name - do nothing ?
1048 }
1049 }
1050
1051 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1052
1053 # normalize dnsbl_sites
1054 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1055 if (scalar(@dnsbl_sites)) {
1056 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1057 }
1058
1059 my $usepolicy = 0;
1060 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1061 $self->get('mail', 'spf');
1062 $vars->{postfix}->{usepolicy} = $usepolicy;
1063
1064 my $resolv = PVE::INotify::read_file('resolvconf');
1065 $vars->{dns}->{hostname} = $nodename;
1066
1067 my $domain = $resolv->{search} // 'localdomain';
1068 $vars->{dns}->{domain} = $domain;
1069
1070 my $wlbr = "$nodename.$domain";
1071 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1072 $wlbr .= " $r"
1073 }
1074 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1075
1076 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1077 eval {
1078 my $uri = URI->new($proxy);
1079 my $host = $uri->host;
1080 my $port = $uri->port // 8080;
1081 if ($host) {
1082 my $data = { host => $host, port => $port };
1083 if (my $ui = $uri->userinfo) {
1084 my ($username, $pw) = split(/:/, $ui, 2);
1085 $data->{username} = $username;
1086 $data->{password} = $pw if defined($pw);
1087 }
1088 $vars->{proxy} = $data;
1089 }
1090 };
1091 warn "parse http_proxy failed - $@" if $@;
1092 }
1093
1094 return $vars;
1095 }
1096
1097 # use one global TT cache
1098 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1099
1100 my $template_toolkit;
1101
1102 sub get_template_toolkit {
1103
1104 return $template_toolkit if $template_toolkit;
1105
1106 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1107
1108 return $template_toolkit;
1109 }
1110
1111 # rewrite file from template
1112 # return true if file has changed
1113 sub rewrite_config_file {
1114 my ($self, $tmplname, $dstfn) = @_;
1115
1116 my $demo = $self->get('admin', 'demo');
1117
1118 if ($demo) {
1119 my $demosrc = "$tmplname.demo";
1120 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
1121 }
1122
1123 my ($perm, $uid, $gid);
1124
1125 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1126 # needed if file contains a HTTPProxyPasswort
1127
1128 $uid = getpwnam('clamav');
1129 $gid = getgrnam('adm');
1130 $perm = 0600;
1131 }
1132
1133 my $tt = get_template_toolkit();
1134
1135 my $vars = $self->get_template_vars();
1136
1137 my $output = '';
1138
1139 $tt->process($tmplname, $vars, \$output) ||
1140 die $tt->error() . "\n";
1141
1142 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1143
1144 return 0 if defined($old) && ($old eq $output); # no change
1145
1146 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1147
1148 if (defined($uid) && defined($gid)) {
1149 chown($uid, $gid, $dstfn);
1150 }
1151
1152 return 1;
1153 }
1154
1155 # rewrite spam configuration
1156 sub rewrite_config_spam {
1157 my ($self) = @_;
1158
1159 my $use_awl = $self->get('spam', 'use_awl');
1160 my $use_bayes = $self->get('spam', 'use_bayes');
1161 my $use_razor = $self->get('spam', 'use_razor');
1162
1163 my $changes = 0;
1164
1165 # delete AW and bayes databases if those features are disabled
1166 if (!$use_awl) {
1167 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1168 }
1169
1170 if (!$use_bayes) {
1171 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1172 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1173 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1174 }
1175
1176 # make sure we have a custom.cf file (else cluster sync fails)
1177 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1178
1179 $changes = 1 if $self->rewrite_config_file(
1180 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1181
1182 $changes = 1 if $self->rewrite_config_file(
1183 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1184
1185 $changes = 1 if $self->rewrite_config_file(
1186 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1187
1188 $changes = 1 if $self->rewrite_config_file(
1189 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1190
1191 if ($use_razor) {
1192 mkdir "/root/.razor";
1193
1194 $changes = 1 if $self->rewrite_config_file(
1195 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1196
1197 if (! -e '/root/.razor/identity') {
1198 eval {
1199 my $timeout = 30;
1200 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1201 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
1202 };
1203 my $err = $@;
1204 syslog('info', "registering razor failed: $err") if $err;
1205 }
1206 }
1207
1208 return $changes;
1209 }
1210
1211 # rewrite ClamAV configuration
1212 sub rewrite_config_clam {
1213 my ($self) = @_;
1214
1215 return $self->rewrite_config_file(
1216 'clamd.conf.in', '/etc/clamav/clamd.conf');
1217 }
1218
1219 sub rewrite_config_freshclam {
1220 my ($self) = @_;
1221
1222 return $self->rewrite_config_file(
1223 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1224 }
1225
1226 sub rewrite_config_postgres {
1227 my ($self) = @_;
1228
1229 my $pgconfdir = "/etc/postgresql/9.6/main";
1230
1231 my $changes = 0;
1232
1233 $changes = 1 if $self->rewrite_config_file(
1234 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1235
1236 $changes = 1 if $self->rewrite_config_file(
1237 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1238
1239 return $changes;
1240 }
1241
1242 # rewrite /root/.forward
1243 sub rewrite_dot_forward {
1244 my ($self) = @_;
1245
1246 my $dstfn = '/root/.forward';
1247
1248 my $email = $self->get('admin', 'email');
1249
1250 my $output = '';
1251 if ($email && $email =~ m/\s*(\S+)\s*/) {
1252 $output = "$1\n";
1253 } else {
1254 # empty .forward does not forward mails (see man local)
1255 }
1256
1257 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1258
1259 return 0 if defined($old) && ($old eq $output); # no change
1260
1261 PVE::Tools::file_set_contents($dstfn, $output);
1262
1263 return 1;
1264 }
1265
1266 my $write_smtp_whitelist = sub {
1267 my ($filename, $data, $action) = @_;
1268
1269 $action = 'OK' if !$action;
1270
1271 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1272 if -f $filename;
1273
1274 my $new = '';
1275 foreach my $k (sort keys %$data) {
1276 $new .= "$k $action\n";
1277 }
1278
1279 return 0 if defined($old) && ($old eq $new); # no change
1280
1281 PVE::Tools::file_set_contents($filename, $new);
1282
1283 PMG::Utils::run_postmap($filename);
1284
1285 return 1;
1286 };
1287
1288 sub rewrite_postfix_whitelist {
1289 my ($rulecache) = @_;
1290
1291 # see man page for regexp_table for postfix regex table format
1292
1293 # we use a hash to avoid duplicate entries in regex tables
1294 my $tolist = {};
1295 my $fromlist = {};
1296 my $clientlist = {};
1297
1298 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1299 my $oclass = ref($obj);
1300 if ($oclass eq 'PMG::RuleDB::Receiver') {
1301 my $addr = PMG::Utils::quote_regex($obj->{address});
1302 $tolist->{"/^$addr\$/"} = 1;
1303 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1304 my $addr = PMG::Utils::quote_regex($obj->{address});
1305 $tolist->{"/^.+\@$addr\$/"} = 1;
1306 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1307 my $addr = $obj->{address};
1308 $addr =~ s|/|\\/|g;
1309 $tolist->{"/^$addr\$/"} = 1;
1310 }
1311 }
1312
1313 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1314 my $oclass = ref($obj);
1315 my $addr = PMG::Utils::quote_regex($obj->{address});
1316 if ($oclass eq 'PMG::RuleDB::EMail') {
1317 my $addr = PMG::Utils::quote_regex($obj->{address});
1318 $fromlist->{"/^$addr\$/"} = 1;
1319 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1320 my $addr = PMG::Utils::quote_regex($obj->{address});
1321 $fromlist->{"/^.+\@$addr\$/"} = 1;
1322 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1323 my $addr = $obj->{address};
1324 $addr =~ s|/|\\/|g;
1325 $fromlist->{"/^$addr\$/"} = 1;
1326 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1327 $clientlist->{$obj->{address}} = 1;
1328 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1329 $clientlist->{$obj->{address}} = 1;
1330 }
1331 }
1332
1333 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1334 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1335 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1336 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1337 };
1338
1339 # rewrite /etc/postfix/*
1340 sub rewrite_config_postfix {
1341 my ($self, $rulecache) = @_;
1342
1343 # make sure we have required files (else postfix start fails)
1344 IO::File->new($transport_map_filename, 'a', 0644);
1345
1346 my $changes = 0;
1347
1348 if ($self->get('mail', 'tls')) {
1349 eval {
1350 PMG::Utils::gen_proxmox_tls_cert();
1351 };
1352 syslog ('info', "generating certificate failed: $@") if $@;
1353 }
1354
1355 $changes = 1 if $self->rewrite_config_file(
1356 'main.cf.in', '/etc/postfix/main.cf');
1357
1358 $changes = 1 if $self->rewrite_config_file(
1359 'master.cf.in', '/etc/postfix/master.cf');
1360
1361 # make sure we have required files (else postfix start fails)
1362 # Note: postmap need a valid /etc/postfix/main.cf configuration
1363 postmap_pmg_domains();
1364 postmap_pmg_transport();
1365 postmap_tls_policy();
1366
1367 rewrite_postfix_whitelist($rulecache) if $rulecache;
1368
1369 # make sure aliases.db is up to date
1370 system('/usr/bin/newaliases');
1371
1372 return $changes;
1373 }
1374
1375 sub rewrite_config {
1376 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1377
1378 $force_restart = {} if ! $force_restart;
1379
1380 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1381 $force_restart->{postfix}) {
1382 PMG::Utils::service_cmd('postfix', 'restart');
1383 }
1384
1385 if ($self->rewrite_dot_forward() && $restart_services) {
1386 # no need to restart anything
1387 }
1388
1389 if ($self->rewrite_config_postgres() && $restart_services) {
1390 # do nothing (too many side effects)?
1391 # does not happen anyways, because config does not change.
1392 }
1393
1394 if (($self->rewrite_config_spam() && $restart_services) ||
1395 $force_restart->{spam}) {
1396 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1397 }
1398
1399 if (($self->rewrite_config_clam() && $restart_services) ||
1400 $force_restart->{clam}) {
1401 PMG::Utils::service_cmd('clamav-daemon', 'restart');
1402 }
1403
1404 if (($self->rewrite_config_freshclam() && $restart_services) ||
1405 $force_restart->{freshclam}) {
1406 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
1407 }
1408 }
1409
1410 1;