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