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