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