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