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