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