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