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