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