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