]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/Config.pm
config: disable advanced statistic filters by default
[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 local $/ = undef; # slurp mode
943
944 my $raw;
945 $raw = <$fh> if defined($fh);
946
947 return PMG::Config::Base->parse_config($filename, $raw);
948 }
949
950 sub write_pmg_conf {
951 my ($filename, $fh, $cfg) = @_;
952
953 my $raw = PMG::Config::Base->write_config($filename, $cfg);
954
955 PVE::Tools::safe_print($filename, $fh, $raw);
956 }
957
958 PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
959 \&read_pmg_conf,
960 \&write_pmg_conf,
961 undef, always_call_parser => 1);
962
963 # parsers/writers for other files
964
965 my $domainsfilename = "/etc/pmg/domains";
966
967 sub postmap_pmg_domains {
968 PMG::Utils::run_postmap($domainsfilename);
969 }
970
971 sub read_pmg_domains {
972 my ($filename, $fh) = @_;
973
974 my $domains = {};
975
976 my $comment = '';
977 if (defined($fh)) {
978 while (defined(my $line = <$fh>)) {
979 chomp $line;
980 next if $line =~ m/^\s*$/;
981 if ($line =~ m/^#(.*)\s*$/) {
982 $comment = $1;
983 next;
984 }
985 if ($line =~ m/^(\S+)\s.*$/) {
986 my $domain = $1;
987 $domains->{$domain} = {
988 domain => $domain, comment => $comment };
989 $comment = '';
990 } else {
991 warn "parse error in '$filename': $line\n";
992 $comment = '';
993 }
994 }
995 }
996
997 return $domains;
998 }
999
1000 sub write_pmg_domains {
1001 my ($filename, $fh, $domains) = @_;
1002
1003 foreach my $domain (sort keys %$domains) {
1004 my $comment = $domains->{$domain}->{comment};
1005 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1006 if defined($comment) && $comment !~ m/^\s*$/;
1007
1008 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
1009 }
1010 }
1011
1012 PVE::INotify::register_file('domains', $domainsfilename,
1013 \&read_pmg_domains,
1014 \&write_pmg_domains,
1015 undef, always_call_parser => 1);
1016
1017 my $dkimdomainsfile = '/etc/pmg/dkim/domains';
1018
1019 PVE::INotify::register_file('dkimdomains', $dkimdomainsfile,
1020 \&read_pmg_domains,
1021 \&write_pmg_domains,
1022 undef, always_call_parser => 1);
1023
1024 my $mynetworks_filename = "/etc/pmg/mynetworks";
1025
1026 sub read_pmg_mynetworks {
1027 my ($filename, $fh) = @_;
1028
1029 my $mynetworks = {};
1030
1031 my $comment = '';
1032 if (defined($fh)) {
1033 while (defined(my $line = <$fh>)) {
1034 chomp $line;
1035 next if $line =~ m/^\s*$/;
1036 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
1037 my ($network, $prefix_size, $comment) = ($1, $2, $3);
1038 my $cidr = "$network/${prefix_size}";
1039 # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
1040 $mynetworks->{$cidr} = {
1041 cidr => $cidr,
1042 network_address => $network,
1043 prefix_size => $prefix_size,
1044 comment => $comment // '',
1045 };
1046 } else {
1047 warn "parse error in '$filename': $line\n";
1048 }
1049 }
1050 }
1051
1052 return $mynetworks;
1053 }
1054
1055 sub write_pmg_mynetworks {
1056 my ($filename, $fh, $mynetworks) = @_;
1057
1058 foreach my $cidr (sort keys %$mynetworks) {
1059 my $data = $mynetworks->{$cidr};
1060 my $comment = $data->{comment} // '*';
1061 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
1062 }
1063 }
1064
1065 PVE::INotify::register_file('mynetworks', $mynetworks_filename,
1066 \&read_pmg_mynetworks,
1067 \&write_pmg_mynetworks,
1068 undef, always_call_parser => 1);
1069
1070 PVE::JSONSchema::register_format(
1071 'tls-policy', \&pmg_verify_tls_policy);
1072
1073 # TODO: extend to parse attributes of the policy
1074 my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1075 sub pmg_verify_tls_policy {
1076 my ($policy, $noerr) = @_;
1077
1078 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1079 return undef if $noerr;
1080 die "value '$policy' does not look like a valid tls policy\n";
1081 }
1082 return $policy;
1083 }
1084
1085 PVE::JSONSchema::register_format(
1086 'tls-policy-strict', \&pmg_verify_tls_policy_strict);
1087
1088 sub pmg_verify_tls_policy_strict {
1089 my ($policy, $noerr) = @_;
1090
1091 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
1092 return undef if $noerr;
1093 die "value '$policy' does not look like a valid tls policy\n";
1094 }
1095 return $policy;
1096 }
1097
1098 PVE::JSONSchema::register_format(
1099 'transport-domain-or-nexthop', \&pmg_verify_transport_domain_or_nexthop);
1100
1101 sub pmg_verify_transport_domain_or_nexthop {
1102 my ($name, $noerr) = @_;
1103
1104 if (pmg_verify_transport_domain($name, 1)) {
1105 return $name;
1106 } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
1107 my $nexthop = $1;
1108 if ($nexthop =~ m/^\[(.*)\]$/) {
1109 $nexthop = $1;
1110 }
1111 return $name if pmg_verify_transport_address($nexthop, 1);
1112 } else {
1113 return undef if $noerr;
1114 die "value does not look like a valid domain or next-hop\n";
1115 }
1116 }
1117
1118 sub read_tls_policy {
1119 my ($filename, $fh) = @_;
1120
1121 return {} if !defined($fh);
1122
1123 my $tls_policy = {};
1124
1125 while (defined(my $line = <$fh>)) {
1126 chomp $line;
1127 next if $line =~ m/^\s*$/;
1128 next if $line =~ m/^#(.*)\s*$/;
1129
1130 my $parse_error = sub {
1131 my ($err) = @_;
1132 warn "parse error in '$filename': $line - $err\n";
1133 };
1134
1135 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1136 my ($destination, $policy) = ($1, $2);
1137
1138 eval {
1139 pmg_verify_transport_domain_or_nexthop($destination);
1140 pmg_verify_tls_policy($policy);
1141 };
1142 if (my $err = $@) {
1143 $parse_error->($err);
1144 next;
1145 }
1146
1147 $tls_policy->{$destination} = {
1148 destination => $destination,
1149 policy => $policy,
1150 };
1151 } else {
1152 $parse_error->('wrong format');
1153 }
1154 }
1155
1156 return $tls_policy;
1157 }
1158
1159 sub write_tls_policy {
1160 my ($filename, $fh, $tls_policy) = @_;
1161
1162 return if !$tls_policy;
1163
1164 foreach my $destination (sort keys %$tls_policy) {
1165 my $entry = $tls_policy->{$destination};
1166 PVE::Tools::safe_print(
1167 $filename, $fh, "$entry->{destination} $entry->{policy}\n");
1168 }
1169 }
1170
1171 my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1172 PVE::INotify::register_file('tls_policy', $tls_policy_map_filename,
1173 \&read_tls_policy,
1174 \&write_tls_policy,
1175 undef, always_call_parser => 1);
1176
1177 sub postmap_tls_policy {
1178 PMG::Utils::run_postmap($tls_policy_map_filename);
1179 }
1180
1181 sub read_tls_inbound_domains {
1182 my ($filename, $fh) = @_;
1183
1184 return {} if !defined($fh);
1185
1186 my $domains = {};
1187
1188 while (defined(my $line = <$fh>)) {
1189 chomp $line;
1190 next if $line =~ m/^\s*$/;
1191 next if $line =~ m/^#(.*)\s*$/;
1192
1193 my $parse_error = sub {
1194 my ($err) = @_;
1195 warn "parse error in '$filename': $line - $err\n";
1196 };
1197
1198 if ($line =~ m/^(\S+) reject_plaintext_session$/) {
1199 my $domain = $1;
1200
1201 eval { pmg_verify_transport_domain($domain) };
1202 if (my $err = $@) {
1203 $parse_error->($err);
1204 next;
1205 }
1206
1207 $domains->{$domain} = 1;
1208 } else {
1209 $parse_error->('wrong format');
1210 }
1211 }
1212
1213 return $domains;
1214 }
1215
1216 sub write_tls_inbound_domains {
1217 my ($filename, $fh, $domains) = @_;
1218
1219 return if !$domains;
1220
1221 foreach my $domain (sort keys %$domains) {
1222 PVE::Tools::safe_print($filename, $fh, "$domain reject_plaintext_session\n");
1223 }
1224 }
1225
1226 my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
1227 PVE::INotify::register_file('tls_inbound_domains', $tls_inbound_domains_map_filename,
1228 \&read_tls_inbound_domains,
1229 \&write_tls_inbound_domains,
1230 undef, always_call_parser => 1);
1231
1232 sub postmap_tls_inbound_domains {
1233 PMG::Utils::run_postmap($tls_inbound_domains_map_filename);
1234 }
1235
1236 my $transport_map_filename = "/etc/pmg/transport";
1237
1238 sub postmap_pmg_transport {
1239 PMG::Utils::run_postmap($transport_map_filename);
1240 }
1241
1242 PVE::JSONSchema::register_format(
1243 'transport-address', \&pmg_verify_transport_address);
1244
1245 sub pmg_verify_transport_address {
1246 my ($name, $noerr) = @_;
1247
1248 if ($name =~ m/^ipv6:($IPV6RE)$/i) {
1249 return $name;
1250 } elsif (PVE::JSONSchema::pve_verify_address($name, 1)) {
1251 return $name;
1252 } else {
1253 return undef if $noerr;
1254 die "value does not look like a valid address\n";
1255 }
1256 }
1257
1258 sub read_transport_map {
1259 my ($filename, $fh) = @_;
1260
1261 return [] if !defined($fh);
1262
1263 my $res = {};
1264
1265 my $comment = '';
1266
1267 while (defined(my $line = <$fh>)) {
1268 chomp $line;
1269 next if $line =~ m/^\s*$/;
1270 if ($line =~ m/^#(.*)\s*$/) {
1271 $comment = $1;
1272 next;
1273 }
1274
1275 my $parse_error = sub {
1276 my ($err) = @_;
1277 warn "parse error in '$filename': $line - $err";
1278 $comment = '';
1279 };
1280
1281 if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) {
1282 my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5);
1283
1284 eval { pmg_verify_transport_domain_or_email($domain); };
1285 if (my $err = $@) {
1286 $parse_error->($err);
1287 next;
1288 }
1289 my $use_mx = 1;
1290 if ($host =~ m/^\[(.*)\]$/) {
1291 $host = $1;
1292 $use_mx = 0;
1293 }
1294 $use_mx = 0 if ($protocol eq "lmtp");
1295
1296 eval { pmg_verify_transport_address($host); };
1297 if (my $err = $@) {
1298 $parse_error->($err);
1299 next;
1300 }
1301
1302 my $data = {
1303 domain => $domain,
1304 protocol => $protocol,
1305 host => $host,
1306 port => $port,
1307 use_mx => $use_mx,
1308 comment => $comment,
1309 };
1310 $res->{$domain} = $data;
1311 $comment = '';
1312 } else {
1313 $parse_error->('wrong format');
1314 }
1315 }
1316
1317 return $res;
1318 }
1319
1320 sub write_transport_map {
1321 my ($filename, $fh, $tmap) = @_;
1322
1323 return if !$tmap;
1324
1325 foreach my $domain (sort keys %$tmap) {
1326 my $data = $tmap->{$domain};
1327
1328 my $comment = $data->{comment};
1329 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1330 if defined($comment) && $comment !~ m/^\s*$/;
1331
1332 my $bracket_host = !$data->{use_mx};
1333
1334 if ($data->{protocol} eq 'lmtp') {
1335 $bracket_host = 0;
1336 $data->{protocol} .= ":inet";
1337 }
1338 $bracket_host = 1 if $data->{host} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
1339 my $host = $bracket_host ? "[$data->{host}]" : $data->{host};
1340
1341 PVE::Tools::safe_print($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
1342 }
1343 }
1344
1345 PVE::INotify::register_file('transport', $transport_map_filename,
1346 \&read_transport_map,
1347 \&write_transport_map,
1348 undef, always_call_parser => 1);
1349
1350 # config file generation using templates
1351
1352 sub get_host_dns_info {
1353 my ($self) = @_;
1354
1355 my $dnsinfo = {};
1356 my $nodename = PVE::INotify::nodename();
1357
1358 $dnsinfo->{hostname} = $nodename;
1359 my $resolv = PVE::INotify::read_file('resolvconf');
1360
1361 my $domain = $resolv->{search} // 'localdomain';
1362 # postfix will not parse a hostname with trailing '.'
1363 $domain =~ s/^(.*)\.$/$1/;
1364 $dnsinfo->{domain} = $domain;
1365
1366 $dnsinfo->{fqdn} = "$nodename.$domain";
1367
1368 return $dnsinfo;
1369 }
1370
1371 sub get_template_vars {
1372 my ($self) = @_;
1373
1374 my $vars = { pmg => $self->get_config() };
1375
1376 my $dnsinfo = get_host_dns_info();
1377 $vars->{dns} = $dnsinfo;
1378 my $int_ip = PMG::Cluster::remote_node_ip($dnsinfo->{hostname});
1379 $vars->{ipconfig}->{int_ip} = $int_ip;
1380
1381 my $transportnets = {};
1382 my $mynetworks = {
1383 '127.0.0.0/8' => 1,
1384 '[::1]/128', => 1,
1385 };
1386
1387 if (my $tmap = PVE::INotify::read_file('transport')) {
1388 foreach my $domain (keys %$tmap) {
1389 my $data = $tmap->{$domain};
1390 my $host = $data->{host};
1391 if ($host =~ m/^$IPV4RE$/) {
1392 $transportnets->{"$host/32"} = 1;
1393 $mynetworks->{"$host/32"} = 1;
1394 } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
1395 $transportnets->{"[$1]/128"} = 1;
1396 $mynetworks->{"[$1]/128"} = 1;
1397 }
1398 }
1399 }
1400
1401 $vars->{postfix}->{transportnets} = join(' ', sort keys %$transportnets);
1402
1403 if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
1404 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
1405 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1406 $mynetworks->{"[$1]/$2"} = 1;
1407 } else {
1408 $mynetworks->{$int_net_cidr} = 1;
1409 }
1410 } else {
1411 if ($int_ip =~ m/^$IPV6RE$/) {
1412 $mynetworks->{"[$int_ip]/128"} = 1;
1413 } else {
1414 $mynetworks->{"$int_ip/32"} = 1;
1415 }
1416 }
1417 }
1418
1419 my $netlist = PVE::INotify::read_file('mynetworks');
1420 foreach my $cidr (keys %$netlist) {
1421 my $ip = PVE::Network::IP_from_cidr($cidr);
1422
1423 if (!$ip) {
1424 warn "failed to parse mynetworks entry '$cidr', ignoring\n";
1425 } elsif ($ip->version() == 4) {
1426 $mynetworks->{$ip->prefix()} = 1;
1427 } else {
1428 my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
1429 $mynetworks->{$address} = 1;
1430 }
1431 }
1432
1433 # add default relay to mynetworks
1434 if (my $relay = $self->get('mail', 'relay')) {
1435 if ($relay =~ m/^$IPV4RE$/) {
1436 $mynetworks->{"$relay/32"} = 1;
1437 } elsif ($relay =~ m/^$IPV6RE$/) {
1438 $mynetworks->{"[$relay]/128"} = 1;
1439 } else {
1440 # DNS name - do nothing ?
1441 }
1442 }
1443
1444 $vars->{postfix}->{mynetworks} = join(' ', sort keys %$mynetworks);
1445
1446 # normalize dnsbl_sites
1447 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1448 if (scalar(@dnsbl_sites)) {
1449 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1450 }
1451
1452 $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
1453
1454 my $usepolicy = 0;
1455 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1456 $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
1457 $vars->{postfix}->{usepolicy} = $usepolicy;
1458
1459 if (!defined($int_ip)) {
1460 warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
1461 $vars->{postfix}->{int_ip} = '127.0.0.1';
1462 } elsif ($int_ip =~ m/^$IPV6RE$/) {
1463 $vars->{postfix}->{int_ip} = "[$int_ip]";
1464 } else {
1465 $vars->{postfix}->{int_ip} = $int_ip;
1466 }
1467
1468 my $wlbr = $dnsinfo->{fqdn};
1469 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1470 $wlbr .= " $r"
1471 }
1472 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1473
1474 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1475 eval {
1476 my $uri = URI->new($proxy);
1477 my $host = $uri->host;
1478 my $port = $uri->port // 8080;
1479 if ($host) {
1480 my $data = { host => $host, port => $port };
1481 if (my $ui = $uri->userinfo) {
1482 my ($username, $pw) = split(/:/, $ui, 2);
1483 $data->{username} = $username;
1484 $data->{password} = $pw if defined($pw);
1485 }
1486 $vars->{proxy} = $data;
1487 }
1488 };
1489 warn "parse http_proxy failed - $@" if $@;
1490 }
1491 $vars->{postgres}->{version} = PMG::Utils::get_pg_server_version();
1492
1493 return $vars;
1494 }
1495
1496 # reads the $filename and checks if it's equal as the $cmp string passed
1497 my sub file_content_equals_str {
1498 my ($filename, $cmp) = @_;
1499
1500 return if !-f $filename;
1501 my $current = PVE::Tools::file_get_contents($filename, 128*1024);
1502 return defined($current) && $current eq $cmp; # no change
1503 }
1504
1505 # use one global TT cache
1506 our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1507
1508 my $template_toolkit;
1509
1510 sub get_template_toolkit {
1511
1512 return $template_toolkit if $template_toolkit;
1513
1514 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1515
1516 return $template_toolkit;
1517 }
1518
1519 # rewrite file from template
1520 # return true if file has changed
1521 sub rewrite_config_file {
1522 my ($self, $tmplname, $dstfn) = @_;
1523
1524 my $demo = $self->get('admin', 'demo');
1525
1526 if ($demo) {
1527 my $demosrc = "$tmplname.demo";
1528 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
1529 }
1530
1531 my ($perm, $uid, $gid);
1532
1533 if ($dstfn eq '/etc/clamav/freshclam.conf') {
1534 # needed if file contains a HTTPProxyPasswort
1535
1536 $uid = getpwnam('clamav');
1537 $gid = getgrnam('adm');
1538 $perm = 0600;
1539 }
1540
1541 my $tt = get_template_toolkit();
1542
1543 my $vars = $self->get_template_vars();
1544
1545 my $output = '';
1546
1547 $tt->process($tmplname, $vars, \$output) || die $tt->error() . "\n";
1548
1549 return 0 if file_content_equals_str($dstfn, $output); # no change -> nothing to do
1550
1551 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1552
1553 if (defined($uid) && defined($gid)) {
1554 chown($uid, $gid, $dstfn);
1555 }
1556
1557 return 1;
1558 }
1559
1560 # rewrite spam configuration
1561 sub rewrite_config_spam {
1562 my ($self) = @_;
1563
1564 my $use_awl = $self->get('spam', 'use_awl');
1565 my $use_bayes = $self->get('spam', 'use_bayes');
1566 my $use_razor = $self->get('spam', 'use_razor');
1567
1568 my $changes = 0;
1569
1570 # delete AW and bayes databases if those features are disabled
1571 if (!$use_awl) {
1572 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1573 }
1574
1575 if (!$use_bayes) {
1576 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1577 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1578 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
1579 }
1580
1581 # make sure we have the custom SA files (else cluster sync fails)
1582 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1583 IO::File->new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644);
1584
1585 $changes = 1 if $self->rewrite_config_file(
1586 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1587
1588 $changes = 1 if $self->rewrite_config_file(
1589 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1590
1591 $changes = 1 if $self->rewrite_config_file(
1592 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1593
1594 $changes = 1 if $self->rewrite_config_file(
1595 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
1596
1597 $changes = 1 if $self->rewrite_config_file(
1598 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
1599
1600 $changes = 1 if $self->rewrite_config_file(
1601 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
1602
1603 if ($use_razor) {
1604 mkdir "/root/.razor";
1605
1606 $changes = 1 if $self->rewrite_config_file(
1607 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1608
1609 if (! -e '/root/.razor/identity') {
1610 eval {
1611 my $timeout = 30;
1612 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1613 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
1614 };
1615 my $err = $@;
1616 syslog('info', "registering razor failed: $err") if $err;
1617 }
1618 }
1619
1620 return $changes;
1621 }
1622
1623 # rewrite ClamAV configuration
1624 sub rewrite_config_clam {
1625 my ($self) = @_;
1626
1627 return $self->rewrite_config_file(
1628 'clamd.conf.in', '/etc/clamav/clamd.conf');
1629 }
1630
1631 sub rewrite_config_freshclam {
1632 my ($self) = @_;
1633
1634 return $self->rewrite_config_file(
1635 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
1636 }
1637
1638 sub rewrite_config_postgres {
1639 my ($self) = @_;
1640
1641 my $pg_maj_version = PMG::Utils::get_pg_server_version();
1642 my $pgconfdir = "/etc/postgresql/$pg_maj_version/main";
1643
1644 my $changes = 0;
1645
1646 $changes = 1 if $self->rewrite_config_file(
1647 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1648
1649 $changes = 1 if $self->rewrite_config_file(
1650 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1651
1652 return $changes;
1653 }
1654
1655 # rewrite /root/.forward
1656 sub rewrite_dot_forward {
1657 my ($self) = @_;
1658
1659 my $dstfn = '/root/.forward';
1660
1661 my $email = $self->get('admin', 'email');
1662
1663 my $output = '';
1664 if ($email && $email =~ m/\s*(\S+)\s*/) {
1665 $output = "$1\n";
1666 } else {
1667 # empty .forward does not forward mails (see man local)
1668 }
1669 return 0 if file_content_equals_str($dstfn, $output); # no change -> nothing to do
1670
1671 PVE::Tools::file_set_contents($dstfn, $output);
1672
1673 return 1;
1674 }
1675
1676 my $write_smtp_whitelist = sub {
1677 my ($filename, $data, $action) = @_;
1678
1679 $action = 'OK' if !$action;
1680
1681 my $new = '';
1682 foreach my $k (sort keys %$data) {
1683 $new .= "$k $action\n";
1684 }
1685 return 0 if file_content_equals_str($filename, $new); # no change -> nothing to do
1686
1687 PVE::Tools::file_set_contents($filename, $new);
1688
1689 PMG::Utils::run_postmap($filename);
1690
1691 return 1;
1692 };
1693
1694 sub rewrite_postfix_whitelist {
1695 my ($rulecache) = @_;
1696
1697 # see man page for regexp_table for postfix regex table format
1698
1699 # we use a hash to avoid duplicate entries in regex tables
1700 my $tolist = {};
1701 my $fromlist = {};
1702 my $clientlist = {};
1703
1704 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1705 my $oclass = ref($obj);
1706 if ($oclass eq 'PMG::RuleDB::Receiver') {
1707 my $addr = PMG::Utils::quote_regex($obj->{address});
1708 $tolist->{"/^$addr\$/"} = 1;
1709 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1710 my $addr = PMG::Utils::quote_regex($obj->{address});
1711 $tolist->{"/^.+\@$addr\$/"} = 1;
1712 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1713 my $addr = $obj->{address};
1714 $addr =~ s|/|\\/|g;
1715 $tolist->{"/^$addr\$/"} = 1;
1716 }
1717 }
1718
1719 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1720 my $oclass = ref($obj);
1721 my $addr = PMG::Utils::quote_regex($obj->{address});
1722 if ($oclass eq 'PMG::RuleDB::EMail') {
1723 my $addr = PMG::Utils::quote_regex($obj->{address});
1724 $fromlist->{"/^$addr\$/"} = 1;
1725 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1726 my $addr = PMG::Utils::quote_regex($obj->{address});
1727 $fromlist->{"/^.+\@$addr\$/"} = 1;
1728 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1729 my $addr = $obj->{address};
1730 $addr =~ s|/|\\/|g;
1731 $fromlist->{"/^$addr\$/"} = 1;
1732 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1733 $clientlist->{$obj->{address}} = 1;
1734 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1735 $clientlist->{$obj->{address}} = 1;
1736 }
1737 }
1738
1739 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1740 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1741 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1742 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1743 };
1744
1745 # rewrite /etc/postfix/*
1746 sub rewrite_config_postfix {
1747 my ($self, $rulecache) = @_;
1748
1749 # make sure we have required files (else postfix start fails)
1750 IO::File->new($transport_map_filename, 'a', 0644);
1751
1752 my $changes = 0;
1753
1754 if ($self->get('mail', 'tls')) {
1755 eval {
1756 PMG::Utils::gen_proxmox_tls_cert();
1757 };
1758 syslog ('info', "generating certificate failed: $@") if $@;
1759 }
1760
1761 $changes = 1 if $self->rewrite_config_file(
1762 'main.cf.in', '/etc/postfix/main.cf');
1763
1764 $changes = 1 if $self->rewrite_config_file(
1765 'master.cf.in', '/etc/postfix/master.cf');
1766
1767 # make sure we have required files (else postfix start fails)
1768 # Note: postmap need a valid /etc/postfix/main.cf configuration
1769 postmap_pmg_domains();
1770 postmap_pmg_transport();
1771 postmap_tls_policy();
1772 postmap_tls_inbound_domains();
1773
1774 rewrite_postfix_whitelist($rulecache) if $rulecache;
1775
1776 # make sure aliases.db is up to date
1777 system('/usr/bin/newaliases');
1778
1779 return $changes;
1780 }
1781
1782 #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter)
1783 my $pmg_service_params = {
1784 mail => {
1785 hide_received => 1,
1786 ndr_on_block => 1,
1787 smtputf8 => 1,
1788 },
1789 admin => {
1790 dkim_selector => 1,
1791 dkim_sign => 1,
1792 dkim_sign_all_mail => 1,
1793 },
1794 };
1795
1796 my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg';
1797 my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck';
1798
1799 sub dump_smtp_filter_config {
1800 my ($self) = @_;
1801
1802 my $conf = '';
1803 my $val;
1804 foreach my $sec (sort keys %$pmg_service_params) {
1805 my $conf_sec = $self->{ids}->{$sec} // {};
1806 foreach my $key (sort keys %{$pmg_service_params->{$sec}}) {
1807 $val = $conf_sec->{$key};
1808 $conf .= "$sec.$key:$val\n" if defined($val);
1809 }
1810 }
1811
1812 return $conf;
1813 }
1814
1815 sub compare_smtp_filter_config {
1816 my ($self) = @_;
1817
1818 my $ret = 0;
1819 my $old;
1820 eval {
1821 $old = PVE::Tools::file_get_contents($smtp_filter_cfg);
1822 };
1823
1824 if (my $err = $@) {
1825 syslog ('warning', "reloading pmg-smtp-filter: $err");
1826 $ret = 1;
1827 } else {
1828 my $new = $self->dump_smtp_filter_config();
1829 $ret = 1 if $old ne $new;
1830 }
1831
1832 $self->write_smtp_filter_config() if $ret;
1833
1834 return $ret;
1835 }
1836
1837 # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison
1838 # on config change
1839 sub write_smtp_filter_config {
1840 my ($self) = @_;
1841
1842 PVE::Tools::lock_file($smtp_filter_cfg_lock, undef, sub {
1843 PVE::Tools::file_set_contents($smtp_filter_cfg,
1844 $self->dump_smtp_filter_config());
1845 });
1846
1847 die $@ if $@;
1848 }
1849
1850 sub rewrite_config {
1851 my ($self, $rulecache, $restart_services, $force_restart) = @_;
1852
1853 $force_restart = {} if ! $force_restart;
1854
1855 my $log_restart = sub {
1856 syslog ('info', "configuration change detected for '$_[0]', restarting");
1857 };
1858
1859 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
1860 $force_restart->{postfix}) {
1861 $log_restart->('postfix');
1862 PMG::Utils::service_cmd('postfix', 'reload');
1863 }
1864
1865 if ($self->rewrite_dot_forward() && $restart_services) {
1866 # no need to restart anything
1867 }
1868
1869 if ($self->rewrite_config_postgres() && $restart_services) {
1870 # do nothing (too many side effects)?
1871 # does not happen anyways, because config does not change.
1872 }
1873
1874 if (($self->rewrite_config_spam() && $restart_services) ||
1875 $force_restart->{spam}) {
1876 $log_restart->('pmg-smtp-filter');
1877 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1878 }
1879
1880 if (($self->rewrite_config_clam() && $restart_services) ||
1881 $force_restart->{clam}) {
1882 $log_restart->('clamav-daemon');
1883 PMG::Utils::service_cmd('clamav-daemon', 'restart');
1884 }
1885
1886 if (($self->rewrite_config_freshclam() && $restart_services) ||
1887 $force_restart->{freshclam}) {
1888 $log_restart->('clamav-freshclam');
1889 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
1890 }
1891
1892 if (($self->compare_smtp_filter_config() && $restart_services) ||
1893 $force_restart->{spam}) {
1894 syslog ('info', "scheduled reload for pmg-smtp-filter");
1895 PMG::Utils::reload_smtp_filter();
1896 }
1897 }
1898
1899 1;