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