]> git.proxmox.com Git - pmg-api.git/blob - PMG/Config.pm
fix #1673: Allow email addresses in transport maps
[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 'transport-domain-or-email', \&pmg_verify_transport_domain_or_email);
654
655 sub pmg_verify_transport_domain_or_email {
656 my ($name, $noerr) = @_;
657
658 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
659
660 # email address
661 if ($name =~ m/^(?:[^\s\/\@]+\@)(${namere}\.)*${namere}$/) {
662 return $name;
663 }
664
665 # like dns-name, but can contain leading dot
666 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
667 return undef if $noerr;
668 die "value does not look like a valid transport domain or email address\n";
669 }
670 return $name;
671 }
672
673 PVE::JSONSchema::register_format(
674 'dnsbl-entry', \&pmg_verify_dnsbl_entry);
675
676 sub pmg_verify_dnsbl_entry {
677 my ($name, $noerr) = @_;
678
679 # like dns-name, but can contain trailing weight: 'domain*<WEIGHT>'
680 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
681
682 if ($name !~ /^(${namere}\.)*${namere}(\*\-?\d+)?$/) {
683 return undef if $noerr;
684 die "value '$name' does not look like a valid dnsbl entry\n";
685 }
686 return $name;
687 }
688
689 sub new {
690 my ($type) = @_;
691
692 my $class = ref($type) || $type;
693
694 my $cfg = PVE::INotify::read_file("pmg.conf");
695
696 return bless $cfg, $class;
697 }
698
699 sub write {
700 my ($self) = @_;
701
702 PVE::INotify::write_file("pmg.conf", $self);
703 }
704
705 my $lockfile = "/var/lock/pmgconfig.lck";
706
707 sub lock_config {
708 my ($code, $errmsg) = @_;
709
710 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
711 if (my $err = $@) {
712 $errmsg ? die "$errmsg: $err" : die $err;
713 }
714 }
715
716 # set section values
717 sub set {
718 my ($self, $section, $key, $value) = @_;
719
720 my $pdata = PMG::Config::Base->private();
721
722 my $plugin = $pdata->{plugins}->{$section};
723 die "no such section '$section'" if !$plugin;
724
725 if (defined($value)) {
726 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
727 $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
728 $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
729 } else {
730 if (defined($self->{ids}->{$section})) {
731 delete $self->{ids}->{$section}->{$key};
732 }
733 }
734
735 return undef;
736 }
737
738 # get section value or default
739 sub get {
740 my ($self, $section, $key, $nodefault) = @_;
741
742 my $pdata = PMG::Config::Base->private();
743 my $pdesc = $pdata->{propertyList}->{$key};
744 die "no such property '$section/$key'\n"
745 if !(defined($pdesc) && defined($pdata->{options}->{$section}) &&
746 defined($pdata->{options}->{$section}->{$key}));
747
748 if (defined($self->{ids}->{$section}) &&
749 defined(my $value = $self->{ids}->{$section}->{$key})) {
750 return $value;
751 }
752
753 return undef if $nodefault;
754
755 return $pdesc->{default};
756 }
757
758 # get a whole section with default value
759 sub get_section {
760 my ($self, $section) = @_;
761
762 my $pdata = PMG::Config::Base->private();
763 return undef if !defined($pdata->{options}->{$section});
764
765 my $res = {};
766
767 foreach my $key (keys %{$pdata->{options}->{$section}}) {
768
769 my $pdesc = $pdata->{propertyList}->{$key};
770
771 if (defined($self->{ids}->{$section}) &&
772 defined(my $value = $self->{ids}->{$section}->{$key})) {
773 $res->{$key} = $value;
774 next;
775 }
776 $res->{$key} = $pdesc->{default};
777 }
778
779 return $res;
780 }
781
782 # get a whole config with default values
783 sub get_config {
784 my ($self) = @_;
785
786 my $pdata = PMG::Config::Base->private();
787
788 my $res = {};
789
790 foreach my $type (keys %{$pdata->{plugins}}) {
791 my $plugin = $pdata->{plugins}->{$type};
792 $res->{$type} = $self->get_section($type);
793 }
794
795 return $res;
796 }
797
798 sub read_pmg_conf {
799 my ($filename, $fh) = @_;
800
801 local $/ = undef; # slurp mode
802
803 my $raw = <$fh> if defined($fh);
804
805 return PMG::Config::Base->parse_config($filename, $raw);
806 }
807
808 sub write_pmg_conf {
809 my ($filename, $fh, $cfg) = @_;
810
811 my $raw = PMG::Config::Base->write_config($filename, $cfg);
812
813 PVE::Tools::safe_print($filename, $fh, $raw);
814 }
815
816 PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
817 \&read_pmg_conf,
818 \&write_pmg_conf,
819 undef, always_call_parser => 1);
820
821 # parsers/writers for other files
822
823 my $domainsfilename = "/etc/pmg/domains";
824
825 sub postmap_pmg_domains {
826 PMG::Utils::run_postmap($domainsfilename);
827 }
828
829 sub read_pmg_domains {
830 my ($filename, $fh) = @_;
831
832 my $domains = {};
833
834 my $comment = '';
835 if (defined($fh)) {
836 while (defined(my $line = <$fh>)) {
837 chomp $line;
838 next if $line =~ m/^\s*$/;
839 if ($line =~ m/^#(.*)\s*$/) {
840 $comment = $1;
841 next;
842 }
843 if ($line =~ m/^(\S+)\s.*$/) {
844 my $domain = $1;
845 $domains->{$domain} = {
846 domain => $domain, comment => $comment };
847 $comment = '';
848 } else {
849 warn "parse error in '$filename': $line\n";
850 $comment = '';
851 }
852 }
853 }
854
855 return $domains;
856 }
857
858 sub write_pmg_domains {
859 my ($filename, $fh, $domains) = @_;
860
861 foreach my $domain (sort keys %$domains) {
862 my $comment = $domains->{$domain}->{comment};
863 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
864 if defined($comment) && $comment !~ m/^\s*$/;
865
866 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
867 }
868 }
869
870 PVE::INotify::register_file('domains', $domainsfilename,
871 \&read_pmg_domains,
872 \&write_pmg_domains,
873 undef, always_call_parser => 1);
874
875 my $mynetworks_filename = "/etc/pmg/mynetworks";
876
877 sub read_pmg_mynetworks {
878 my ($filename, $fh) = @_;
879
880 my $mynetworks = {};
881
882 my $comment = '';
883 if (defined($fh)) {
884 while (defined(my $line = <$fh>)) {
885 chomp $line;
886 next if $line =~ m/^\s*$/;
887 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
888 my ($network, $prefix_size, $comment) = ($1, $2, $3);
889 my $cidr = "$network/${prefix_size}";
890 $mynetworks->{$cidr} = {
891 cidr => $cidr,
892 network_address => $network,
893 prefix_size => $prefix_size,
894 comment => $comment // '',
895 };
896 } else {
897 warn "parse error in '$filename': $line\n";
898 }
899 }
900 }
901
902 return $mynetworks;
903 }
904
905 sub write_pmg_mynetworks {
906 my ($filename, $fh, $mynetworks) = @_;
907
908 foreach my $cidr (sort keys %$mynetworks) {
909 my $data = $mynetworks->{$cidr};
910 my $comment = $data->{comment} // '*';
911 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
912 }
913 }
914
915 PVE::INotify::register_file('mynetworks', $mynetworks_filename,
916 \&read_pmg_mynetworks,
917 \&write_pmg_mynetworks,
918 undef, always_call_parser => 1);
919
920 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
921
922 sub postmap_tls_policy {
923 PMG::Utils::run_postmap($tls_policy_map_filename);
924 }
925
926 my $transport_map_filename = "/etc/pmg/transport";
927
928 sub postmap_pmg_transport {
929 PMG::Utils::run_postmap($transport_map_filename);
930 }
931
932 sub read_transport_map {
933 my ($filename, $fh) = @_;
934
935 return [] if !defined($fh);
936
937 my $res = {};
938
939 my $comment = '';
940
941 while (defined(my $line = <$fh>)) {
942 chomp $line;
943 next if $line =~ m/^\s*$/;
944 if ($line =~ m/^#(.*)\s*$/) {
945 $comment = $1;
946 next;
947 }
948
949 my $parse_error = sub {
950 my ($err) = @_;
951 warn "parse error in '$filename': $line - $err";
952 $comment = '';
953 };
954
955 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
956 my ($domain, $host, $port) = ($1, $2, $3);
957
958 eval { pmg_verify_transport_domain_or_email($domain); };
959 if (my $err = $@) {
960 $parse_error->($err);
961 next;
962 }
963 my $use_mx = 1;
964 if ($host =~ m/^\[(.*)\]$/) {
965 $host = $1;
966 $use_mx = 0;
967 }
968
969 eval { PVE::JSONSchema::pve_verify_address($host); };
970 if (my $err = $@) {
971 $parse_error->($err);
972 next;
973 }
974
975 my $data = {
976 domain => $domain,
977 host => $host,
978 port => $port,
979 use_mx => $use_mx,
980 comment => $comment,
981 };
982 $res->{$domain} = $data;
983 $comment = '';
984 } else {
985 $parse_error->('wrong format');
986 }
987 }
988
989 return $res;
990 }
991
992 sub write_transport_map {
993 my ($filename, $fh, $tmap) = @_;
994
995 return if !$tmap;
996
997 foreach my $domain (sort keys %$tmap) {
998 my $data = $tmap->{$domain};
999
1000 my $comment = $data->{comment};
1001 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1002 if defined($comment) && $comment !~ m/^\s*$/;
1003
1004 my $use_mx = $data->{use_mx};
1005 $use_mx = 0 if $data->{host} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1006
1007 if ($use_mx) {
1008 PVE::Tools::safe_print(
1009 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
1010 } else {
1011 PVE::Tools::safe_print(
1012 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
1013 }
1014 }
1015 }
1016
1017 PVE::INotify::register_file('transport', $transport_map_filename,
1018 \&read_transport_map,
1019 \&write_transport_map,
1020 undef, always_call_parser => 1);
1021
1022 # config file generation using templates
1023
1024 sub get_template_vars {
1025 my ($self) = @_;
1026
1027 my $vars = { pmg => $self->get_config() };
1028
1029 my $nodename = PVE::INotify::nodename();
1030 my $int_ip = PMG::Cluster::remote_node_ip($nodename);
1031 $vars->{ipconfig}->{int_ip} = $int_ip;
1032
1033 my $transportnets = [];
1034
1035 if (my $tmap = PVE::INotify::read_file('transport')) {
1036 foreach my $domain (sort keys %$tmap) {
1037 my $data = $tmap->{$domain};
1038 my $host = $data->{host};
1039 if ($host =~ m/^$IPV4RE$/) {
1040 push @$transportnets, "$host/32";
1041 } elsif ($host =~ m/^$IPV6RE$/) {
1042 push @$transportnets, "[$host]/128";
1043 }
1044 }
1045 }
1046
1047 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
1048
1049 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1050
1051 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
1052 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1053 push @$mynetworks, "[$1]/$2";
1054 } else {
1055 push @$mynetworks, $int_net_cidr;
1056 }
1057 } else {
1058 if ($int_ip =~ m/^$IPV6RE$/) {
1059 push @$mynetworks, "[$int_ip]/128";
1060 } else {
1061 push @$mynetworks, "$int_ip/32";
1062 }
1063 }
1064
1065 my $netlist = PVE::INotify::read_file('mynetworks');
1066 foreach my $cidr (keys %$netlist) {
1067 if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1068 push @$mynetworks, "[$1]/$2";
1069 } else {
1070 push @$mynetworks, $cidr;
1071 }
1072 }
1073
1074 push @$mynetworks, @$transportnets;
1075
1076 # add default relay to mynetworks
1077 if (my $relay = $self->get('mail', 'relay')) {
1078 if ($relay =~ m/^$IPV4RE$/) {
1079 push @$mynetworks, "$relay/32";
1080 } elsif ($relay =~ m/^$IPV6RE$/) {
1081 push @$mynetworks, "[$relay]/128";
1082 } else {
1083 # DNS name - do nothing ?
1084 }
1085 }
1086
1087 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1088
1089 # normalize dnsbl_sites
1090 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1091 if (scalar(@dnsbl_sites)) {
1092 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1093 }
1094
1095 my $usepolicy = 0;
1096 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1097 $self->get('mail', 'spf');
1098 $vars->{postfix}->{usepolicy} = $usepolicy;
1099
1100 if ($int_ip =~ m/^$IPV6RE$/) {
1101 $vars->{postfix}->{int_ip} = "[$int_ip]";
1102 } else {
1103 $vars->{postfix}->{int_ip} = $int_ip;
1104 }
1105
1106 my $resolv = PVE::INotify::read_file('resolvconf');
1107 $vars->{dns}->{hostname} = $nodename;
1108
1109 my $domain = $resolv->{search} // 'localdomain';
1110 $vars->{dns}->{domain} = $domain;
1111
1112 my $wlbr = "$nodename.$domain";
1113 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1114 $wlbr .= " $r"
1115 }
1116 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1117
1118 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1119 eval {
1120 my $uri = URI->new($proxy);
1121 my $host = $uri->host;
1122 my $port = $uri->port // 8080;
1123 if ($host) {
1124 my $data = { host => $host, port => $port };
1125 if (my $ui = $uri->userinfo) {
1126 my ($username, $pw) = split(/:/, $ui, 2);
1127 $data->{username} = $username;
1128 $data->{password} = $pw if defined($pw);
1129 }
1130 $vars->{proxy} = $data;
1131 }
1132 };
1133 warn "parse http_proxy failed - $@" if $@;
1134 }
1135
1136 return $vars;
1137 }
1138
1139 # use one global TT cache
1140 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1141
1142 my $template_toolkit;
1143
1144 sub get_template_toolkit {
1145
1146 return $template_toolkit if $template_toolkit;
1147
1148 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1149
1150 return $template_toolkit;
1151 }
1152
1153 # rewrite file from template
1154 # return true if file has changed
1155 sub rewrite_config_file {
1156 my ($self, $tmplname, $dstfn) = @_;
1157
1158 my $demo = $self->get('admin', 'demo');
1159
1160 if ($demo) {
1161 my $demosrc = "$tmplname.demo";
1162 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
1163 }
1164
1165 my ($perm, $uid, $gid);
1166
1167 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1168 # needed if file contains a HTTPProxyPasswort
1169
1170 $uid = getpwnam('clamav');
1171 $gid = getgrnam('adm');
1172 $perm = 0600;
1173 }
1174
1175 my $tt = get_template_toolkit();
1176
1177 my $vars = $self->get_template_vars();
1178
1179 my $output = '';
1180
1181 $tt->process($tmplname, $vars, \$output) ||
1182 die $tt->error() . "\n";
1183
1184 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1185
1186 return 0 if defined($old) && ($old eq $output); # no change
1187
1188 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1189
1190 if (defined($uid) && defined($gid)) {
1191 chown($uid, $gid, $dstfn);
1192 }
1193
1194 return 1;
1195 }
1196
1197 # rewrite spam configuration
1198 sub rewrite_config_spam {
1199 my ($self) = @_;
1200
1201 my $use_awl = $self->get('spam', 'use_awl');
1202 my $use_bayes = $self->get('spam', 'use_bayes');
1203 my $use_razor = $self->get('spam', 'use_razor');
1204
1205 my $changes = 0;
1206
1207 # delete AW and bayes databases if those features are disabled
1208 if (!$use_awl) {
1209 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1210 }
1211
1212 if (!$use_bayes) {
1213 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1214 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1215 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1216 }
1217
1218 # make sure we have a custom.cf file (else cluster sync fails)
1219 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1220
1221 $changes = 1 if $self->rewrite_config_file(
1222 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1223
1224 $changes = 1 if $self->rewrite_config_file(
1225 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1226
1227 $changes = 1 if $self->rewrite_config_file(
1228 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1229
1230 $changes = 1 if $self->rewrite_config_file(
1231 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1232
1233 if ($use_razor) {
1234 mkdir "/root/.razor";
1235
1236 $changes = 1 if $self->rewrite_config_file(
1237 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1238
1239 if (! -e '/root/.razor/identity') {
1240 eval {
1241 my $timeout = 30;
1242 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1243 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
1244 };
1245 my $err = $@;
1246 syslog('info', "registering razor failed: $err") if $err;
1247 }
1248 }
1249
1250 return $changes;
1251 }
1252
1253 # rewrite ClamAV configuration
1254 sub rewrite_config_clam {
1255 my ($self) = @_;
1256
1257 return $self->rewrite_config_file(
1258 'clamd.conf.in', '/etc/clamav/clamd.conf');
1259 }
1260
1261 sub rewrite_config_freshclam {
1262 my ($self) = @_;
1263
1264 return $self->rewrite_config_file(
1265 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1266 }
1267
1268 sub rewrite_config_postgres {
1269 my ($self) = @_;
1270
1271 my $pgconfdir = "/etc/postgresql/9.6/main";
1272
1273 my $changes = 0;
1274
1275 $changes = 1 if $self->rewrite_config_file(
1276 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1277
1278 $changes = 1 if $self->rewrite_config_file(
1279 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1280
1281 return $changes;
1282 }
1283
1284 # rewrite /root/.forward
1285 sub rewrite_dot_forward {
1286 my ($self) = @_;
1287
1288 my $dstfn = '/root/.forward';
1289
1290 my $email = $self->get('admin', 'email');
1291
1292 my $output = '';
1293 if ($email && $email =~ m/\s*(\S+)\s*/) {
1294 $output = "$1\n";
1295 } else {
1296 # empty .forward does not forward mails (see man local)
1297 }
1298
1299 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1300
1301 return 0 if defined($old) && ($old eq $output); # no change
1302
1303 PVE::Tools::file_set_contents($dstfn, $output);
1304
1305 return 1;
1306 }
1307
1308 my $write_smtp_whitelist = sub {
1309 my ($filename, $data, $action) = @_;
1310
1311 $action = 'OK' if !$action;
1312
1313 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1314 if -f $filename;
1315
1316 my $new = '';
1317 foreach my $k (sort keys %$data) {
1318 $new .= "$k $action\n";
1319 }
1320
1321 return 0 if defined($old) && ($old eq $new); # no change
1322
1323 PVE::Tools::file_set_contents($filename, $new);
1324
1325 PMG::Utils::run_postmap($filename);
1326
1327 return 1;
1328 };
1329
1330 sub rewrite_postfix_whitelist {
1331 my ($rulecache) = @_;
1332
1333 # see man page for regexp_table for postfix regex table format
1334
1335 # we use a hash to avoid duplicate entries in regex tables
1336 my $tolist = {};
1337 my $fromlist = {};
1338 my $clientlist = {};
1339
1340 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1341 my $oclass = ref($obj);
1342 if ($oclass eq 'PMG::RuleDB::Receiver') {
1343 my $addr = PMG::Utils::quote_regex($obj->{address});
1344 $tolist->{"/^$addr\$/"} = 1;
1345 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1346 my $addr = PMG::Utils::quote_regex($obj->{address});
1347 $tolist->{"/^.+\@$addr\$/"} = 1;
1348 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1349 my $addr = $obj->{address};
1350 $addr =~ s|/|\\/|g;
1351 $tolist->{"/^$addr\$/"} = 1;
1352 }
1353 }
1354
1355 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1356 my $oclass = ref($obj);
1357 my $addr = PMG::Utils::quote_regex($obj->{address});
1358 if ($oclass eq 'PMG::RuleDB::EMail') {
1359 my $addr = PMG::Utils::quote_regex($obj->{address});
1360 $fromlist->{"/^$addr\$/"} = 1;
1361 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1362 my $addr = PMG::Utils::quote_regex($obj->{address});
1363 $fromlist->{"/^.+\@$addr\$/"} = 1;
1364 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1365 my $addr = $obj->{address};
1366 $addr =~ s|/|\\/|g;
1367 $fromlist->{"/^$addr\$/"} = 1;
1368 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1369 $clientlist->{$obj->{address}} = 1;
1370 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1371 $clientlist->{$obj->{address}} = 1;
1372 }
1373 }
1374
1375 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1376 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1377 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1378 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1379 };
1380
1381 # rewrite /etc/postfix/*
1382 sub rewrite_config_postfix {
1383 my ($self, $rulecache) = @_;
1384
1385 # make sure we have required files (else postfix start fails)
1386 IO::File->new($transport_map_filename, 'a', 0644);
1387
1388 my $changes = 0;
1389
1390 if ($self->get('mail', 'tls')) {
1391 eval {
1392 PMG::Utils::gen_proxmox_tls_cert();
1393 };
1394 syslog ('info', "generating certificate failed: $@") if $@;
1395 }
1396
1397 $changes = 1 if $self->rewrite_config_file(
1398 'main.cf.in', '/etc/postfix/main.cf');
1399
1400 $changes = 1 if $self->rewrite_config_file(
1401 'master.cf.in', '/etc/postfix/master.cf');
1402
1403 # make sure we have required files (else postfix start fails)
1404 # Note: postmap need a valid /etc/postfix/main.cf configuration
1405 postmap_pmg_domains();
1406 postmap_pmg_transport();
1407 postmap_tls_policy();
1408
1409 rewrite_postfix_whitelist($rulecache) if $rulecache;
1410
1411 # make sure aliases.db is up to date
1412 system('/usr/bin/newaliases');
1413
1414 return $changes;
1415 }
1416
1417 sub rewrite_config {
1418 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1419
1420 $force_restart = {} if ! $force_restart;
1421
1422 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1423 $force_restart->{postfix}) {
1424 PMG::Utils::service_cmd('postfix', 'restart');
1425 }
1426
1427 if ($self->rewrite_dot_forward() && $restart_services) {
1428 # no need to restart anything
1429 }
1430
1431 if ($self->rewrite_config_postgres() && $restart_services) {
1432 # do nothing (too many side effects)?
1433 # does not happen anyways, because config does not change.
1434 }
1435
1436 if (($self->rewrite_config_spam() && $restart_services) ||
1437 $force_restart->{spam}) {
1438 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1439 }
1440
1441 if (($self->rewrite_config_clam() && $restart_services) ||
1442 $force_restart->{clam}) {
1443 PMG::Utils::service_cmd('clamav-daemon', 'restart');
1444 }
1445
1446 if (($self->rewrite_config_freshclam() && $restart_services) ||
1447 $force_restart->{freshclam}) {
1448 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
1449 }
1450 }
1451
1452 1;