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