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