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