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