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