]> git.proxmox.com Git - pmg-api.git/blob - PMG/Config.pm
also load SpamAssassin descriptions from local dir
[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 use_rbl => {
505 description => "Use Realtime Blacklists.",
506 type => 'boolean',
507 default => 1,
508 },
509 tls => {
510 description => "Enable TLS.",
511 type => 'boolean',
512 default => 0,
513 },
514 tlslog => {
515 description => "Enable TLS Logging.",
516 type => 'boolean',
517 default => 0,
518 },
519 tlsheader => {
520 description => "Add TLS received header.",
521 type => 'boolean',
522 default => 0,
523 },
524 spf => {
525 description => "Use Sender Policy Framework.",
526 type => 'boolean',
527 default => 1,
528 },
529 greylist => {
530 description => "Use Greylisting.",
531 type => 'boolean',
532 default => 1,
533 },
534 helotests => {
535 description => "Use SMTP HELO tests.",
536 type => 'boolean',
537 default => 0,
538 },
539 rejectunknown => {
540 description => "Reject unknown clients.",
541 type => 'boolean',
542 default => 0,
543 },
544 rejectunknownsender => {
545 description => "Reject unknown senders.",
546 type => 'boolean',
547 default => 0,
548 },
549 verifyreceivers => {
550 description => "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
551 type => 'string',
552 enum => ['450', '550'],
553 },
554 dnsbl_sites => {
555 description => "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
556 type => 'string',
557 },
558 };
559 }
560
561 sub options {
562 return {
563 int_port => { optional => 1 },
564 ext_port => { optional => 1 },
565 smarthost => { optional => 1 },
566 relay => { optional => 1 },
567 relayport => { optional => 1 },
568 relaynomx => { optional => 1 },
569 dwarning => { optional => 1 },
570 max_smtpd_in => { optional => 1 },
571 max_smtpd_out => { optional => 1 },
572 greylist => { optional => 1 },
573 helotests => { optional => 1 },
574 use_rbl => { optional => 1 },
575 tls => { optional => 1 },
576 tlslog => { optional => 1 },
577 tlsheader => { optional => 1 },
578 spf => { optional => 1 },
579 maxsize => { optional => 1 },
580 banner => { optional => 1 },
581 max_filters => { optional => 1 },
582 max_policy => { optional => 1 },
583 hide_received => { optional => 1 },
584 rejectunknown => { optional => 1 },
585 rejectunknownsender => { optional => 1 },
586 conn_count_limit => { optional => 1 },
587 conn_rate_limit => { optional => 1 },
588 message_rate_limit => { optional => 1 },
589 verifyreceivers => { optional => 1 },
590 dnsbl_sites => { optional => 1 },
591 };
592 }
593
594 package PMG::Config;
595
596 use strict;
597 use warnings;
598 use IO::File;
599 use Data::Dumper;
600 use Template;
601
602 use PVE::SafeSyslog;
603 use PVE::Tools qw($IPV4RE $IPV6RE);
604 use PVE::INotify;
605 use PVE::JSONSchema;
606
607 use PMG::Cluster;
608
609 PMG::Config::Admin->register();
610 PMG::Config::Mail->register();
611 PMG::Config::SpamQuarantine->register();
612 PMG::Config::VirusQuarantine->register();
613 PMG::Config::Spam->register();
614 PMG::Config::ClamAV->register();
615
616 # initialize all plugins
617 PMG::Config::Base->init();
618
619 PVE::JSONSchema::register_format(
620 'transport-domain', \&pmg_verify_transport_domain);
621 sub pmg_verify_transport_domain {
622 my ($name, $noerr) = @_;
623
624 # like dns-name, but can contain leading dot
625 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
626
627 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
628 return undef if $noerr;
629 die "value does not look like a valid transport domain\n";
630 }
631 return $name;
632 }
633
634 sub new {
635 my ($type) = @_;
636
637 my $class = ref($type) || $type;
638
639 my $cfg = PVE::INotify::read_file("pmg.conf");
640
641 return bless $cfg, $class;
642 }
643
644 sub write {
645 my ($self) = @_;
646
647 PVE::INotify::write_file("pmg.conf", $self);
648 }
649
650 my $lockfile = "/var/lock/pmgconfig.lck";
651
652 sub lock_config {
653 my ($code, $errmsg) = @_;
654
655 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
656 if (my $err = $@) {
657 $errmsg ? die "$errmsg: $err" : die $err;
658 }
659 }
660
661 # set section values
662 sub set {
663 my ($self, $section, $key, $value) = @_;
664
665 my $pdata = PMG::Config::Base->private();
666
667 my $plugin = $pdata->{plugins}->{$section};
668 die "no such section '$section'" if !$plugin;
669
670 if (defined($value)) {
671 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
672 $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
673 $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
674 } else {
675 if (defined($self->{ids}->{$section})) {
676 delete $self->{ids}->{$section}->{$key};
677 }
678 }
679
680 return undef;
681 }
682
683 # get section value or default
684 sub get {
685 my ($self, $section, $key, $nodefault) = @_;
686
687 my $pdata = PMG::Config::Base->private();
688 my $pdesc = $pdata->{propertyList}->{$key};
689 die "no such property '$section/$key'\n"
690 if !(defined($pdesc) && defined($pdata->{options}->{$section}) &&
691 defined($pdata->{options}->{$section}->{$key}));
692
693 if (defined($self->{ids}->{$section}) &&
694 defined(my $value = $self->{ids}->{$section}->{$key})) {
695 return $value;
696 }
697
698 return undef if $nodefault;
699
700 return $pdesc->{default};
701 }
702
703 # get a whole section with default value
704 sub get_section {
705 my ($self, $section) = @_;
706
707 my $pdata = PMG::Config::Base->private();
708 return undef if !defined($pdata->{options}->{$section});
709
710 my $res = {};
711
712 foreach my $key (keys %{$pdata->{options}->{$section}}) {
713
714 my $pdesc = $pdata->{propertyList}->{$key};
715
716 if (defined($self->{ids}->{$section}) &&
717 defined(my $value = $self->{ids}->{$section}->{$key})) {
718 $res->{$key} = $value;
719 next;
720 }
721 $res->{$key} = $pdesc->{default};
722 }
723
724 return $res;
725 }
726
727 # get a whole config with default values
728 sub get_config {
729 my ($self) = @_;
730
731 my $pdata = PMG::Config::Base->private();
732
733 my $res = {};
734
735 foreach my $type (keys %{$pdata->{plugins}}) {
736 my $plugin = $pdata->{plugins}->{$type};
737 $res->{$type} = $self->get_section($type);
738 }
739
740 return $res;
741 }
742
743 sub read_pmg_conf {
744 my ($filename, $fh) = @_;
745
746 local $/ = undef; # slurp mode
747
748 my $raw = <$fh> if defined($fh);
749
750 return PMG::Config::Base->parse_config($filename, $raw);
751 }
752
753 sub write_pmg_conf {
754 my ($filename, $fh, $cfg) = @_;
755
756 my $raw = PMG::Config::Base->write_config($filename, $cfg);
757
758 PVE::Tools::safe_print($filename, $fh, $raw);
759 }
760
761 PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
762 \&read_pmg_conf,
763 \&write_pmg_conf,
764 undef, always_call_parser => 1);
765
766 # parsers/writers for other files
767
768 my $domainsfilename = "/etc/pmg/domains";
769
770 sub postmap_pmg_domains {
771 PMG::Utils::run_postmap($domainsfilename);
772 }
773
774 sub read_pmg_domains {
775 my ($filename, $fh) = @_;
776
777 my $domains = {};
778
779 my $comment = '';
780 if (defined($fh)) {
781 while (defined(my $line = <$fh>)) {
782 chomp $line;
783 next if $line =~ m/^\s*$/;
784 if ($line =~ m/^#(.*)\s*$/) {
785 $comment = $1;
786 next;
787 }
788 if ($line =~ m/^(\S+)\s.*$/) {
789 my $domain = $1;
790 $domains->{$domain} = {
791 domain => $domain, comment => $comment };
792 $comment = '';
793 } else {
794 warn "parse error in '$filename': $line\n";
795 $comment = '';
796 }
797 }
798 }
799
800 return $domains;
801 }
802
803 sub write_pmg_domains {
804 my ($filename, $fh, $domains) = @_;
805
806 foreach my $domain (sort keys %$domains) {
807 my $comment = $domains->{$domain}->{comment};
808 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
809 if defined($comment) && $comment !~ m/^\s*$/;
810
811 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
812 }
813 }
814
815 PVE::INotify::register_file('domains', $domainsfilename,
816 \&read_pmg_domains,
817 \&write_pmg_domains,
818 undef, always_call_parser => 1);
819
820 my $mynetworks_filename = "/etc/pmg/mynetworks";
821
822 sub postmap_pmg_mynetworks {
823 PMG::Utils::run_postmap($mynetworks_filename);
824 }
825
826 sub read_pmg_mynetworks {
827 my ($filename, $fh) = @_;
828
829 my $mynetworks = {};
830
831 my $comment = '';
832 if (defined($fh)) {
833 while (defined(my $line = <$fh>)) {
834 chomp $line;
835 next if $line =~ m/^\s*$/;
836 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
837 my ($network, $prefix_size, $comment) = ($1, $2, $3);
838 my $cidr = "$network/${prefix_size}";
839 $mynetworks->{$cidr} = {
840 cidr => $cidr,
841 network_address => $network,
842 prefix_size => $prefix_size,
843 comment => $comment // '',
844 };
845 } else {
846 warn "parse error in '$filename': $line\n";
847 }
848 }
849 }
850
851 return $mynetworks;
852 }
853
854 sub write_pmg_mynetworks {
855 my ($filename, $fh, $mynetworks) = @_;
856
857 foreach my $cidr (sort keys %$mynetworks) {
858 my $data = $mynetworks->{$cidr};
859 my $comment = $data->{comment} // '*';
860 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
861 }
862 }
863
864 PVE::INotify::register_file('mynetworks', $mynetworks_filename,
865 \&read_pmg_mynetworks,
866 \&write_pmg_mynetworks,
867 undef, always_call_parser => 1);
868
869 my $transport_map_filename = "/etc/pmg/transport";
870
871 sub postmap_pmg_transport {
872 PMG::Utils::run_postmap($transport_map_filename);
873 }
874
875 sub read_transport_map {
876 my ($filename, $fh) = @_;
877
878 return [] if !defined($fh);
879
880 my $res = {};
881
882 my $comment = '';
883
884 while (defined(my $line = <$fh>)) {
885 chomp $line;
886 next if $line =~ m/^\s*$/;
887 if ($line =~ m/^#(.*)\s*$/) {
888 $comment = $1;
889 next;
890 }
891
892 my $parse_error = sub {
893 my ($err) = @_;
894 warn "parse error in '$filename': $line - $err";
895 $comment = '';
896 };
897
898 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
899 my ($domain, $host, $port) = ($1, $2, $3);
900
901 eval { pmg_verify_transport_domain($domain); };
902 if (my $err = $@) {
903 $parse_error->($err);
904 next;
905 }
906 my $use_mx = 1;
907 if ($host =~ m/^\[(.*)\]$/) {
908 $host = $1;
909 $use_mx = 0;
910 }
911
912 eval { PVE::JSONSchema::pve_verify_address($host); };
913 if (my $err = $@) {
914 $parse_error->($err);
915 next;
916 }
917
918 my $data = {
919 domain => $domain,
920 host => $host,
921 port => $port,
922 use_mx => $use_mx,
923 comment => $comment,
924 };
925 $res->{$domain} = $data;
926 $comment = '';
927 } else {
928 $parse_error->('wrong format');
929 }
930 }
931
932 return $res;
933 }
934
935 sub write_transport_map {
936 my ($filename, $fh, $tmap) = @_;
937
938 return if !$tmap;
939
940 foreach my $domain (sort keys %$tmap) {
941 my $data = $tmap->{$domain};
942
943 my $comment = $data->{comment};
944 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
945 if defined($comment) && $comment !~ m/^\s*$/;
946
947 my $use_mx = $data->{use_mx};
948 $use_mx = 0 if $data->{host} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
949
950 if ($use_mx) {
951 PVE::Tools::safe_print(
952 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
953 } else {
954 PVE::Tools::safe_print(
955 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
956 }
957 }
958 }
959
960 PVE::INotify::register_file('transport', $transport_map_filename,
961 \&read_transport_map,
962 \&write_transport_map,
963 undef, always_call_parser => 1);
964
965 # config file generation using templates
966
967 sub get_template_vars {
968 my ($self) = @_;
969
970 my $vars = { pmg => $self->get_config() };
971
972 my $nodename = PVE::INotify::nodename();
973 my $int_ip = PMG::Cluster::remote_node_ip($nodename);
974 my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip);
975 $vars->{ipconfig}->{int_ip} = $int_ip;
976 # $vars->{ipconfig}->{int_net_cidr} = $int_net_cidr;
977
978 my $transportnets = [];
979
980 my $tmap = PVE::INotify::read_file('transport');
981 foreach my $domain (sort keys %$tmap) {
982 my $data = $tmap->{$domain};
983 my $host = $data->{host};
984 if ($host =~ m/^$IPV4RE$/) {
985 push @$transportnets, "$host/32";
986 } elsif ($host =~ m/^$IPV6RE$/) {
987 push @$transportnets, "[$host]/128";
988 }
989 }
990
991 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
992
993 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
994 push @$mynetworks, @$transportnets;
995 push @$mynetworks, $int_net_cidr;
996 push @$mynetworks, 'hash:/etc/pmg/mynetworks';
997
998 my $netlist = PVE::INotify::read_file('mynetworks');
999 # add default relay to mynetworks
1000 if (my $relay = $self->get('mail', 'relay')) {
1001 if ($relay =~ m/^$IPV4RE$/) {
1002 push @$mynetworks, "$relay/32";
1003 } elsif ($relay =~ m/^$IPV6RE$/) {
1004 push @$mynetworks, "[$relay]/128";
1005 } else {
1006 # DNS name - do nothing ?
1007 }
1008 }
1009
1010 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1011
1012 my $usepolicy = 0;
1013 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1014 $self->get('mail', 'spf') || $self->get('mail', 'use_rbl');
1015 $vars->{postfix}->{usepolicy} = $usepolicy;
1016
1017 my $resolv = PVE::INotify::read_file('resolvconf');
1018 $vars->{dns}->{hostname} = $nodename;
1019 $vars->{dns}->{domain} = $resolv->{search};
1020
1021 my $wlbr = "$nodename.$resolv->{search}";
1022 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1023 $wlbr .= " $r"
1024 }
1025 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1026
1027 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1028 eval {
1029 my $uri = URI->new($proxy);
1030 my $host = $uri->host;
1031 my $port = $uri->port // 8080;
1032 if ($host) {
1033 my $data = { host => $host, port => $port };
1034 if (my $ui = $uri->userinfo) {
1035 my ($username, $pw) = split(/:/, $ui, 2);
1036 $data->{username} = $username;
1037 $data->{password} = $pw if defined($pw);
1038 }
1039 $vars->{proxy} = $data;
1040 }
1041 };
1042 warn "parse http_proxy failed - $@" if $@;
1043 }
1044
1045 return $vars;
1046 }
1047
1048 # use one global TT cache
1049 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1050
1051 my $template_toolkit;
1052
1053 sub get_template_toolkit {
1054
1055 return $template_toolkit if $template_toolkit;
1056
1057 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1058
1059 return $template_toolkit;
1060 }
1061
1062 # rewrite file from template
1063 # return true if file has changed
1064 sub rewrite_config_file {
1065 my ($self, $tmplname, $dstfn) = @_;
1066
1067 my $demo = $self->get('admin', 'demo');
1068
1069 if ($demo) {
1070 my $demosrc = "$tmplname.demo";
1071 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
1072 }
1073
1074 my ($perm, $uid, $gid);
1075
1076 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1077 # needed if file contains a HTTPProxyPasswort
1078
1079 $uid = getpwnam('clamav');
1080 $gid = getgrnam('adm');
1081 $perm = 0600;
1082 }
1083
1084 my $tt = get_template_toolkit();
1085
1086 my $vars = $self->get_template_vars();
1087
1088 my $output = '';
1089
1090 $tt->process($tmplname, $vars, \$output) ||
1091 die $tt->error() . "\n";
1092
1093 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1094
1095 return 0 if defined($old) && ($old eq $output); # no change
1096
1097 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1098
1099 if (defined($uid) && defined($gid)) {
1100 chown($uid, $gid, $dstfn);
1101 }
1102
1103 return 1;
1104 }
1105
1106 # rewrite spam configuration
1107 sub rewrite_config_spam {
1108 my ($self) = @_;
1109
1110 my $use_awl = $self->get('spam', 'use_awl');
1111 my $use_bayes = $self->get('spam', 'use_bayes');
1112 my $use_razor = $self->get('spam', 'use_razor');
1113
1114 my $changes = 0;
1115
1116 # delete AW and bayes databases if those features are disabled
1117 if (!$use_awl) {
1118 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1119 }
1120
1121 if (!$use_bayes) {
1122 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1123 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1124 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1125 }
1126
1127 # make sure we have a custom.cf file (else cluster sync fails)
1128 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1129
1130 $changes = 1 if $self->rewrite_config_file(
1131 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1132
1133 $changes = 1 if $self->rewrite_config_file(
1134 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1135
1136 $changes = 1 if $self->rewrite_config_file(
1137 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1138
1139 $changes = 1 if $self->rewrite_config_file(
1140 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1141
1142 if ($use_razor) {
1143 mkdir "/root/.razor";
1144
1145 $changes = 1 if $self->rewrite_config_file(
1146 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1147
1148 if (! -e '/root/.razor/identity') {
1149 eval {
1150 my $timeout = 30;
1151 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1152 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
1153 };
1154 my $err = $@;
1155 syslog('info', "registering razor failed: $err") if $err;
1156 }
1157 }
1158
1159 return $changes;
1160 }
1161
1162 # rewrite ClamAV configuration
1163 sub rewrite_config_clam {
1164 my ($self) = @_;
1165
1166 return $self->rewrite_config_file(
1167 'clamd.conf.in', '/etc/clamav/clamd.conf');
1168 }
1169
1170 sub rewrite_config_freshclam {
1171 my ($self) = @_;
1172
1173 return $self->rewrite_config_file(
1174 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1175 }
1176
1177 sub rewrite_config_postgres {
1178 my ($self) = @_;
1179
1180 my $pgconfdir = "/etc/postgresql/9.6/main";
1181
1182 my $changes = 0;
1183
1184 $changes = 1 if $self->rewrite_config_file(
1185 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1186
1187 $changes = 1 if $self->rewrite_config_file(
1188 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1189
1190 return $changes;
1191 }
1192
1193 # rewrite /root/.forward
1194 sub rewrite_dot_forward {
1195 my ($self) = @_;
1196
1197 my $dstfn = '/root/.forward';
1198
1199 my $email = $self->get('admin', 'email');
1200
1201 my $output = '';
1202 if ($email && $email =~ m/\s*(\S+)\s*/) {
1203 $output = "$1\n";
1204 } else {
1205 # empty .forward does not forward mails (see man local)
1206 }
1207
1208 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1209
1210 return 0 if defined($old) && ($old eq $output); # no change
1211
1212 PVE::Tools::file_set_contents($dstfn, $output);
1213
1214 return 1;
1215 }
1216
1217 my $write_smtp_whitelist = sub {
1218 my ($filename, $data, $action) = @_;
1219
1220 $action = 'OK' if !$action;
1221
1222 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1223 if -f $filename;
1224
1225 my $new = '';
1226 foreach my $k (sort keys %$data) {
1227 $new .= "$k $action\n";
1228 }
1229
1230 return 0 if defined($old) && ($old eq $new); # no change
1231
1232 PVE::Tools::file_set_contents($filename, $new);
1233
1234 PMG::Utils::run_postmap($filename);
1235
1236 return 1;
1237 };
1238
1239 my $rewrite_config_whitelist = sub {
1240 my ($rulecache) = @_;
1241
1242 # see man page for regexp_table for postfix regex table format
1243
1244 # we use a hash to avoid duplicate entries in regex tables
1245 my $tolist = {};
1246 my $fromlist = {};
1247 my $clientlist = {};
1248
1249 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1250 my $oclass = ref($obj);
1251 if ($oclass eq 'PMG::RuleDB::Receiver') {
1252 my $addr = PMG::Utils::quote_regex($obj->{address});
1253 $tolist->{"/^$addr\$/"} = 1;
1254 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1255 my $addr = PMG::Utils::quote_regex($obj->{address});
1256 $tolist->{"/^.+\@$addr\$/"} = 1;
1257 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1258 my $addr = $obj->{address};
1259 $addr =~ s|/|\\/|g;
1260 $tolist->{"/^$addr\$/"} = 1;
1261 }
1262 }
1263
1264 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1265 my $oclass = ref($obj);
1266 my $addr = PMG::Utils::quote_regex($obj->{address});
1267 if ($oclass eq 'PMG::RuleDB::EMail') {
1268 my $addr = PMG::Utils::quote_regex($obj->{address});
1269 $fromlist->{"/^$addr\$/"} = 1;
1270 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1271 my $addr = PMG::Utils::quote_regex($obj->{address});
1272 $fromlist->{"/^.+\@$addr\$/"} = 1;
1273 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1274 my $addr = $obj->{address};
1275 $addr =~ s|/|\\/|g;
1276 $fromlist->{"/^$addr\$/"} = 1;
1277 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1278 $clientlist->{$obj->{address}} = 1;
1279 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1280 $clientlist->{$obj->{address}} = 1;
1281 }
1282 }
1283
1284 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1285 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1286 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1287 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1288 };
1289
1290 # rewrite /etc/postfix/*
1291 sub rewrite_config_postfix {
1292 my ($self, $rulecache) = @_;
1293
1294 # make sure we have required files (else postfix start fails)
1295 postmap_pmg_domains();
1296 postmap_pmg_transport();
1297 postmap_pmg_mynetworks();
1298
1299 IO::File->new($transport_map_filename, 'a', 0644);
1300
1301 my $changes = 0;
1302
1303 if ($self->get('mail', 'tls')) {
1304 eval {
1305 PMG::Utils::gen_proxmox_tls_cert();
1306 };
1307 syslog ('info', "generating certificate failed: $@") if $@;
1308 }
1309
1310 $changes = 1 if $self->rewrite_config_file(
1311 'main.cf.in', '/etc/postfix/main.cf');
1312
1313 $changes = 1 if $self->rewrite_config_file(
1314 'master.cf.in', '/etc/postfix/master.cf');
1315
1316 $rewrite_config_whitelist->($rulecache) if $rulecache;
1317
1318 # fixme: rewrite_config_tls_policy ($class);
1319
1320 # make sure aliases.db is up to date
1321 system('/usr/bin/newaliases');
1322
1323 return $changes;
1324 }
1325
1326 sub rewrite_config {
1327 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1328
1329 $force_restart = {} if ! $force_restart;
1330
1331 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1332 $force_restart->{postfix}) {
1333 PMG::Utils::service_cmd('postfix', 'restart');
1334 }
1335
1336 if ($self->rewrite_dot_forward() && $restart_services) {
1337 # no need to restart anything
1338 }
1339
1340 if ($self->rewrite_config_postgres() && $restart_services) {
1341 # do nothing (too many side effects)?
1342 # does not happen anyways, because config does not change.
1343 }
1344
1345 if (($self->rewrite_config_spam() && $restart_services) ||
1346 $force_restart->{spam}) {
1347 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1348 }
1349
1350 if (($self->rewrite_config_clam() && $restart_services) ||
1351 $force_restart->{clam}) {
1352 PMG::Utils::service_cmd('clamav-daemon', 'restart');
1353 }
1354
1355 if (($self->rewrite_config_freshclam() && $restart_services) ||
1356 $force_restart->{freshclam}) {
1357 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
1358 }
1359 }
1360
1361 1;