]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/Config.pm
better postgres 11 compat in config/pidfile path references
[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 => {
aa995d5d 334 description => "Whether to block encrypted archives. Mark encrypted archives 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
DM
663use PMG::Cluster;
664
ac5d1312 665PMG::Config::Admin->register();
d9dc3c08 666PMG::Config::Mail->register();
fc070a06
DM
667PMG::Config::SpamQuarantine->register();
668PMG::Config::VirusQuarantine->register();
7e0e6dbe 669PMG::Config::Spam->register();
f62194b2 670PMG::Config::ClamAV->register();
7e0e6dbe
DM
671
672# initialize all plugins
673PMG::Config::Base->init();
674
b86ac4eb
DM
675PVE::JSONSchema::register_format(
676 'transport-domain', \&pmg_verify_transport_domain);
a3051049 677
b86ac4eb
DM
678sub pmg_verify_transport_domain {
679 my ($name, $noerr) = @_;
680
681 # like dns-name, but can contain leading dot
682 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
683
684 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
685 return undef if $noerr;
686 die "value does not look like a valid transport domain\n";
687 }
688 return $name;
689}
f62194b2 690
22c25daf
DM
691PVE::JSONSchema::register_format(
692 'transport-domain-or-email', \&pmg_verify_transport_domain_or_email);
693
694sub pmg_verify_transport_domain_or_email {
695 my ($name, $noerr) = @_;
696
697 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
698
699 # email address
700 if ($name =~ m/^(?:[^\s\/\@]+\@)(${namere}\.)*${namere}$/) {
701 return $name;
702 }
703
704 # like dns-name, but can contain leading dot
705 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
706 return undef if $noerr;
707 die "value does not look like a valid transport domain or email address\n";
708 }
709 return $name;
710}
711
a3051049
DM
712PVE::JSONSchema::register_format(
713 'dnsbl-entry', \&pmg_verify_dnsbl_entry);
714
715sub pmg_verify_dnsbl_entry {
716 my ($name, $noerr) = @_;
717
9f165e3f
SI
718 # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
719 # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
720 # we don't implement the ';' separated numbers in pattern, because this
721 # breaks at PVE::JSONSchema::split_list
a3051049
DM
722 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
723
9f165e3f
SI
724 my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
725 my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
726 if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
a3051049 727 return undef if $noerr;
2c30ee0d 728 die "value '$name' does not look like a valid dnsbl entry\n";
a3051049
DM
729 }
730 return $name;
731}
732
f62194b2
DM
733sub new {
734 my ($type) = @_;
735
736 my $class = ref($type) || $type;
737
738 my $cfg = PVE::INotify::read_file("pmg.conf");
739
740 return bless $cfg, $class;
741}
742
be6e2db9
DM
743sub write {
744 my ($self) = @_;
745
746 PVE::INotify::write_file("pmg.conf", $self);
747}
748
f21d933c
DM
749my $lockfile = "/var/lock/pmgconfig.lck";
750
751sub lock_config {
752 my ($code, $errmsg) = @_;
753
754 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
755 if (my $err = $@) {
756 $errmsg ? die "$errmsg: $err" : die $err;
757 }
758}
759
062f0498 760# set section values
062f0498
DM
761sub set {
762 my ($self, $section, $key, $value) = @_;
763
764 my $pdata = PMG::Config::Base->private();
765
062f0498
DM
766 my $plugin = $pdata->{plugins}->{$section};
767 die "no such section '$section'" if !$plugin;
768
062f0498
DM
769 if (defined($value)) {
770 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
d79b9b0c
DM
771 $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
772 $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
062f0498 773 } else {
d79b9b0c
DM
774 if (defined($self->{ids}->{$section})) {
775 delete $self->{ids}->{$section}->{$key};
062f0498
DM
776 }
777 }
778
779 return undef;
780}
781
f62194b2 782# get section value or default
f62194b2 783sub get {
11081cf6 784 my ($self, $section, $key, $nodefault) = @_;
f62194b2
DM
785
786 my $pdata = PMG::Config::Base->private();
f62194b2 787 my $pdesc = $pdata->{propertyList}->{$key};
3d9837d9
DM
788 die "no such property '$section/$key'\n"
789 if !(defined($pdesc) && defined($pdata->{options}->{$section}) &&
790 defined($pdata->{options}->{$section}->{$key}));
f62194b2 791
d79b9b0c
DM
792 if (defined($self->{ids}->{$section}) &&
793 defined(my $value = $self->{ids}->{$section}->{$key})) {
f62194b2 794 return $value;
1ccc8e95 795 }
f62194b2 796
11081cf6
DM
797 return undef if $nodefault;
798
f62194b2
DM
799 return $pdesc->{default};
800}
801
1ccc8e95 802# get a whole section with default value
1ccc8e95
DM
803sub get_section {
804 my ($self, $section) = @_;
805
806 my $pdata = PMG::Config::Base->private();
807 return undef if !defined($pdata->{options}->{$section});
808
809 my $res = {};
810
811 foreach my $key (keys %{$pdata->{options}->{$section}}) {
812
813 my $pdesc = $pdata->{propertyList}->{$key};
814
d79b9b0c
DM
815 if (defined($self->{ids}->{$section}) &&
816 defined(my $value = $self->{ids}->{$section}->{$key})) {
1ccc8e95
DM
817 $res->{$key} = $value;
818 next;
819 }
820 $res->{$key} = $pdesc->{default};
821 }
822
823 return $res;
824}
825
be16be07 826# get a whole config with default values
be16be07
DM
827sub get_config {
828 my ($self) = @_;
829
9dab5fe5
DM
830 my $pdata = PMG::Config::Base->private();
831
be16be07
DM
832 my $res = {};
833
9dab5fe5 834 foreach my $type (keys %{$pdata->{plugins}}) {
9dab5fe5
DM
835 my $plugin = $pdata->{plugins}->{$type};
836 $res->{$type} = $self->get_section($type);
be16be07
DM
837 }
838
839 return $res;
840}
841
7e0e6dbe
DM
842sub read_pmg_conf {
843 my ($filename, $fh) = @_;
f62194b2 844
7e0e6dbe 845 local $/ = undef; # slurp mode
f62194b2 846
9dfe7c16 847 my $raw = <$fh> if defined($fh);
7e0e6dbe
DM
848
849 return PMG::Config::Base->parse_config($filename, $raw);
850}
851
852sub write_pmg_conf {
853 my ($filename, $fh, $cfg) = @_;
854
855 my $raw = PMG::Config::Base->write_config($filename, $cfg);
856
857 PVE::Tools::safe_print($filename, $fh, $raw);
858}
859
3278b571 860PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
f62194b2 861 \&read_pmg_conf,
9dfe7c16
DM
862 \&write_pmg_conf,
863 undef, always_call_parser => 1);
7e0e6dbe 864
f609bf7f
DM
865# parsers/writers for other files
866
3278b571 867my $domainsfilename = "/etc/pmg/domains";
f609bf7f 868
c3f4336c
DM
869sub postmap_pmg_domains {
870 PMG::Utils::run_postmap($domainsfilename);
871}
872
f609bf7f
DM
873sub read_pmg_domains {
874 my ($filename, $fh) = @_;
875
b7298186 876 my $domains = {};
f609bf7f 877
b7298186 878 my $comment = '';
f609bf7f
DM
879 if (defined($fh)) {
880 while (defined(my $line = <$fh>)) {
3118b703
DM
881 chomp $line;
882 next if $line =~ m/^\s*$/;
b7298186
DM
883 if ($line =~ m/^#(.*)\s*$/) {
884 $comment = $1;
885 next;
886 }
887 if ($line =~ m/^(\S+)\s.*$/) {
f609bf7f 888 my $domain = $1;
b7298186
DM
889 $domains->{$domain} = {
890 domain => $domain, comment => $comment };
891 $comment = '';
3118b703
DM
892 } else {
893 warn "parse error in '$filename': $line\n";
894 $comment = '';
f609bf7f
DM
895 }
896 }
897 }
898
899 return $domains;
900}
901
902sub write_pmg_domains {
b7298186
DM
903 my ($filename, $fh, $domains) = @_;
904
905 foreach my $domain (sort keys %$domains) {
906 my $comment = $domains->{$domain}->{comment};
907 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
908 if defined($comment) && $comment !~ m/^\s*$/;
f609bf7f 909
6b31da64 910 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
f609bf7f
DM
911 }
912}
913
914PVE::INotify::register_file('domains', $domainsfilename,
915 \&read_pmg_domains,
916 \&write_pmg_domains,
917 undef, always_call_parser => 1);
918
bef31f06
DM
919my $mynetworks_filename = "/etc/pmg/mynetworks";
920
bef31f06
DM
921sub read_pmg_mynetworks {
922 my ($filename, $fh) = @_;
923
924 my $mynetworks = {};
925
926 my $comment = '';
927 if (defined($fh)) {
928 while (defined(my $line = <$fh>)) {
929 chomp $line;
930 next if $line =~ m/^\s*$/;
931 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
932 my ($network, $prefix_size, $comment) = ($1, $2, $3);
933 my $cidr = "$network/${prefix_size}";
934 $mynetworks->{$cidr} = {
935 cidr => $cidr,
936 network_address => $network,
937 prefix_size => $prefix_size,
938 comment => $comment // '',
939 };
940 } else {
941 warn "parse error in '$filename': $line\n";
942 }
943 }
944 }
945
946 return $mynetworks;
947}
948
949sub write_pmg_mynetworks {
950 my ($filename, $fh, $mynetworks) = @_;
951
952 foreach my $cidr (sort keys %$mynetworks) {
953 my $data = $mynetworks->{$cidr};
954 my $comment = $data->{comment} // '*';
955 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
956 }
957}
958
959PVE::INotify::register_file('mynetworks', $mynetworks_filename,
960 \&read_pmg_mynetworks,
961 \&write_pmg_mynetworks,
962 undef, always_call_parser => 1);
963
1b449731
SI
964PVE::JSONSchema::register_format(
965 'tls-policy', \&pmg_verify_tls_policy);
966
550f4c47
SI
967# TODO: extend to parse attributes of the policy
968my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
1b449731
SI
969sub pmg_verify_tls_policy {
970 my ($policy, $noerr) = @_;
971
550f4c47 972 if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
1b449731
SI
973 return undef if $noerr;
974 die "value '$policy' does not look like a valid tls policy\n";
975 }
976 return $policy;
977}
978
f1a44c5c
DM
979PVE::JSONSchema::register_format(
980 'tls-policy-strict', \&pmg_verify_tls_policy_strict);
550f4c47 981
f1a44c5c
DM
982sub pmg_verify_tls_policy_strict {
983 my ($policy, $noerr) = @_;
550f4c47 984
f1a44c5c
DM
985 if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
986 return undef if $noerr;
987 die "value '$policy' does not look like a valid tls policy\n";
988 }
989 return $policy;
550f4c47
SI
990}
991
1b449731
SI
992sub read_tls_policy {
993 my ($filename, $fh) = @_;
994
995 return {} if !defined($fh);
996
997 my $tls_policy = {};
998
999 while (defined(my $line = <$fh>)) {
1000 chomp $line;
1001 next if $line =~ m/^\s*$/;
1002 next if $line =~ m/^#(.*)\s*$/;
1003
1004 my $parse_error = sub {
1005 my ($err) = @_;
dfbfa155 1006 die "parse error in '$filename': $line - $err";
1b449731
SI
1007 };
1008
1009 if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
1010 my ($domain, $policy) = ($1, $2);
1011
1012 eval {
1013 pmg_verify_transport_domain($domain);
1014 pmg_verify_tls_policy($policy);
1015 };
1016 if (my $err = $@) {
1017 $parse_error->($err);
1018 next;
1019 }
1020
1021 $tls_policy->{$domain} = {
1022 domain => $domain,
1023 policy => $policy,
1024 };
1025 } else {
1026 $parse_error->('wrong format');
1027 }
1028 }
1029
1030 return $tls_policy;
1031}
1032
1033sub write_tls_policy {
1034 my ($filename, $fh, $tls_policy) = @_;
1035
1036 return if !$tls_policy;
1037
1038 foreach my $domain (sort keys %$tls_policy) {
1039 my $entry = $tls_policy->{$domain};
1040 PVE::Tools::safe_print(
1041 $filename, $fh, "$entry->{domain} $entry->{policy}\n");
1042 }
1043}
1044
959aaeba 1045my $tls_policy_map_filename = "/etc/pmg/tls_policy";
1b449731
SI
1046PVE::INotify::register_file('tls_policy', $tls_policy_map_filename,
1047 \&read_tls_policy,
1048 \&write_tls_policy,
1049 undef, always_call_parser => 1);
959aaeba
DM
1050
1051sub postmap_tls_policy {
1052 PMG::Utils::run_postmap($tls_policy_map_filename);
1053}
1054
cd533938 1055my $transport_map_filename = "/etc/pmg/transport";
3546daf0 1056
3118b703
DM
1057sub postmap_pmg_transport {
1058 PMG::Utils::run_postmap($transport_map_filename);
1059}
1060
3546daf0
DM
1061sub read_transport_map {
1062 my ($filename, $fh) = @_;
1063
1064 return [] if !defined($fh);
1065
1066 my $res = {};
1067
3118b703 1068 my $comment = '';
b7c49fec 1069
3546daf0
DM
1070 while (defined(my $line = <$fh>)) {
1071 chomp $line;
1072 next if $line =~ m/^\s*$/;
3118b703
DM
1073 if ($line =~ m/^#(.*)\s*$/) {
1074 $comment = $1;
1075 next;
1076 }
3546daf0 1077
b7c49fec
DM
1078 my $parse_error = sub {
1079 my ($err) = @_;
1080 warn "parse error in '$filename': $line - $err";
1081 $comment = '';
1082 };
1083
ba323310 1084 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
3118b703 1085 my ($domain, $host, $port) = ($1, $2, $3);
3546daf0 1086
22c25daf 1087 eval { pmg_verify_transport_domain_or_email($domain); };
b7c49fec
DM
1088 if (my $err = $@) {
1089 $parse_error->($err);
1090 next;
1091 }
53904163 1092 my $use_mx = 1;
3546daf0
DM
1093 if ($host =~ m/^\[(.*)\]$/) {
1094 $host = $1;
53904163 1095 $use_mx = 0;
3546daf0
DM
1096 }
1097
b7c49fec
DM
1098 eval { PVE::JSONSchema::pve_verify_address($host); };
1099 if (my $err = $@) {
1100 $parse_error->($err);
1101 next;
1102 }
1103
3118b703
DM
1104 my $data = {
1105 domain => $domain,
1106 host => $host,
1107 port => $port,
53904163 1108 use_mx => $use_mx,
3118b703
DM
1109 comment => $comment,
1110 };
1111 $res->{$domain} = $data;
1112 $comment = '';
1113 } else {
b7c49fec 1114 $parse_error->('wrong format');
3546daf0
DM
1115 }
1116 }
1117
3118b703 1118 return $res;
3546daf0
DM
1119}
1120
cd533938 1121sub write_transport_map {
3546daf0
DM
1122 my ($filename, $fh, $tmap) = @_;
1123
1124 return if !$tmap;
1125
3118b703
DM
1126 foreach my $domain (sort keys %$tmap) {
1127 my $data = $tmap->{$domain};
3546daf0 1128
3118b703
DM
1129 my $comment = $data->{comment};
1130 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
1131 if defined($comment) && $comment !~ m/^\s*$/;
1132
ba323310
DM
1133 my $use_mx = $data->{use_mx};
1134 $use_mx = 0 if $data->{host} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
1135
1136 if ($use_mx) {
3118b703 1137 PVE::Tools::safe_print(
53904163 1138 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
3118b703
DM
1139 } else {
1140 PVE::Tools::safe_print(
53904163 1141 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
3546daf0
DM
1142 }
1143 }
1144}
1145
1146PVE::INotify::register_file('transport', $transport_map_filename,
1147 \&read_transport_map,
cd533938 1148 \&write_transport_map,
3546daf0 1149 undef, always_call_parser => 1);
7e0e6dbe 1150
4ccdc564
DM
1151# config file generation using templates
1152
07b3face
DM
1153sub get_template_vars {
1154 my ($self) = @_;
4ccdc564
DM
1155
1156 my $vars = { pmg => $self->get_config() };
1157
f609bf7f
DM
1158 my $nodename = PVE::INotify::nodename();
1159 my $int_ip = PMG::Cluster::remote_node_ip($nodename);
f609bf7f 1160 $vars->{ipconfig}->{int_ip} = $int_ip;
f609bf7f 1161
ba323310
DM
1162 my $transportnets = [];
1163
5e37e665
DM
1164 if (my $tmap = PVE::INotify::read_file('transport')) {
1165 foreach my $domain (sort keys %$tmap) {
1166 my $data = $tmap->{$domain};
1167 my $host = $data->{host};
1168 if ($host =~ m/^$IPV4RE$/) {
1169 push @$transportnets, "$host/32";
1170 } elsif ($host =~ m/^$IPV6RE$/) {
1171 push @$transportnets, "[$host]/128";
1172 }
ba323310
DM
1173 }
1174 }
1175
f609bf7f
DM
1176 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
1177
1178 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
1e88a529
DM
1179
1180 if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
136e6c92
DM
1181 if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1182 push @$mynetworks, "[$1]/$2";
1183 } else {
1184 push @$mynetworks, $int_net_cidr;
1185 }
1e88a529
DM
1186 } else {
1187 if ($int_ip =~ m/^$IPV6RE$/) {
136e6c92 1188 push @$mynetworks, "[$int_ip]/128";
1e88a529
DM
1189 } else {
1190 push @$mynetworks, "$int_ip/32";
1191 }
1192 }
f609bf7f 1193
bef31f06 1194 my $netlist = PVE::INotify::read_file('mynetworks');
b8733ec1 1195 foreach my $cidr (sort keys %$netlist) {
136e6c92
DM
1196 if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
1197 push @$mynetworks, "[$1]/$2";
1198 } else {
1199 push @$mynetworks, $cidr;
1200 }
1201 }
6d473888
DM
1202
1203 push @$mynetworks, @$transportnets;
1204
f609bf7f
DM
1205 # add default relay to mynetworks
1206 if (my $relay = $self->get('mail', 'relay')) {
ba323310 1207 if ($relay =~ m/^$IPV4RE$/) {
f609bf7f 1208 push @$mynetworks, "$relay/32";
ba323310 1209 } elsif ($relay =~ m/^$IPV6RE$/) {
f609bf7f
DM
1210 push @$mynetworks, "[$relay]/128";
1211 } else {
66af5153 1212 # DNS name - do nothing ?
f609bf7f
DM
1213 }
1214 }
1215
1216 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1217
20125a71
DM
1218 # normalize dnsbl_sites
1219 my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
1220 if (scalar(@dnsbl_sites)) {
1221 $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
1222 }
1223
11247512
AP
1224 $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
1225
f609bf7f
DM
1226 my $usepolicy = 0;
1227 $usepolicy = 1 if $self->get('mail', 'greylist') ||
0eae95d3 1228 $self->get('mail', 'spf');
f609bf7f
DM
1229 $vars->{postfix}->{usepolicy} = $usepolicy;
1230
2664d3cb
DM
1231 if ($int_ip =~ m/^$IPV6RE$/) {
1232 $vars->{postfix}->{int_ip} = "[$int_ip]";
1233 } else {
1234 $vars->{postfix}->{int_ip} = $int_ip;
1235 }
1236
f609bf7f
DM
1237 my $resolv = PVE::INotify::read_file('resolvconf');
1238 $vars->{dns}->{hostname} = $nodename;
f609bf7f 1239
a2c4c2c0
DM
1240 my $domain = $resolv->{search} // 'localdomain';
1241 $vars->{dns}->{domain} = $domain;
1242
1243 my $wlbr = "$nodename.$domain";
ed5fa523
DM
1244 foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
1245 $wlbr .= " $r"
1246 }
1247 $vars->{composed}->{wl_bounce_relays} = $wlbr;
1248
11081cf6
DM
1249 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1250 eval {
1251 my $uri = URI->new($proxy);
1252 my $host = $uri->host;
1253 my $port = $uri->port // 8080;
1254 if ($host) {
1255 my $data = { host => $host, port => $port };
1256 if (my $ui = $uri->userinfo) {
1257 my ($username, $pw) = split(/:/, $ui, 2);
1258 $data->{username} = $username;
1259 $data->{password} = $pw if defined($pw);
1260 }
1261 $vars->{proxy} = $data;
1262 }
1263 };
1264 warn "parse http_proxy failed - $@" if $@;
1265 }
1266
07b3face
DM
1267 return $vars;
1268}
1269
310daf18
DM
1270# use one global TT cache
1271our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1272
1273my $template_toolkit;
1274
1275sub get_template_toolkit {
1276
1277 return $template_toolkit if $template_toolkit;
1278
1279 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1280
1281 return $template_toolkit;
1282}
1283
07b3face
DM
1284# rewrite file from template
1285# return true if file has changed
1286sub rewrite_config_file {
1287 my ($self, $tmplname, $dstfn) = @_;
1288
1289 my $demo = $self->get('admin', 'demo');
1290
07b3face 1291 if ($demo) {
310daf18
DM
1292 my $demosrc = "$tmplname.demo";
1293 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
07b3face
DM
1294 }
1295
c248d69f 1296 my ($perm, $uid, $gid);
07b3face 1297
d1156caa 1298 if ($dstfn eq '/etc/clamav/freshclam.conf') {
07b3face
DM
1299 # needed if file contains a HTTPProxyPasswort
1300
1301 $uid = getpwnam('clamav');
1302 $gid = getgrnam('adm');
1303 $perm = 0600;
1304 }
1305
310daf18 1306 my $tt = get_template_toolkit();
07b3face
DM
1307
1308 my $vars = $self->get_template_vars();
1309
c248d69f 1310 my $output = '';
07b3face 1311
310daf18 1312 $tt->process($tmplname, $vars, \$output) ||
60f82a46 1313 die $tt->error() . "\n";
07b3face
DM
1314
1315 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1316
1317 return 0 if defined($old) && ($old eq $output); # no change
1318
1319 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1320
1321 if (defined($uid) && defined($gid)) {
1322 chown($uid, $gid, $dstfn);
1323 }
1324
1325 return 1;
4ccdc564
DM
1326}
1327
9123cab5
DM
1328# rewrite spam configuration
1329sub rewrite_config_spam {
1330 my ($self) = @_;
1331
1332 my $use_awl = $self->get('spam', 'use_awl');
1333 my $use_bayes = $self->get('spam', 'use_bayes');
1334 my $use_razor = $self->get('spam', 'use_razor');
1335
17424665
DM
1336 my $changes = 0;
1337
9123cab5 1338 # delete AW and bayes databases if those features are disabled
17424665
DM
1339 if (!$use_awl) {
1340 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1341 }
1342
9123cab5 1343 if (!$use_bayes) {
17424665
DM
1344 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1345 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1346 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
9123cab5
DM
1347 }
1348
1349 # make sure we have a custom.cf file (else cluster sync fails)
1350 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1351
17424665
DM
1352 $changes = 1 if $self->rewrite_config_file(
1353 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1354
1355 $changes = 1 if $self->rewrite_config_file(
1356 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1357
1358 $changes = 1 if $self->rewrite_config_file(
1359 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1360
1361 $changes = 1 if $self->rewrite_config_file(
1362 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
9123cab5
DM
1363
1364 if ($use_razor) {
1365 mkdir "/root/.razor";
17424665
DM
1366
1367 $changes = 1 if $self->rewrite_config_file(
1368 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1369
9123cab5
DM
1370 if (! -e '/root/.razor/identity') {
1371 eval {
1372 my $timeout = 30;
17424665
DM
1373 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1374 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
9123cab5
DM
1375 };
1376 my $err = $@;
b902c0b8 1377 syslog('info', "registering razor failed: $err") if $err;
9123cab5
DM
1378 }
1379 }
17424665
DM
1380
1381 return $changes;
9123cab5
DM
1382}
1383
ac5d1312
DM
1384# rewrite ClamAV configuration
1385sub rewrite_config_clam {
1386 my ($self) = @_;
1387
17424665
DM
1388 return $self->rewrite_config_file(
1389 'clamd.conf.in', '/etc/clamav/clamd.conf');
1390}
1391
1392sub rewrite_config_freshclam {
1393 my ($self) = @_;
1394
1395 return $self->rewrite_config_file(
1396 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
ac5d1312
DM
1397}
1398
86737f12
DM
1399sub rewrite_config_postgres {
1400 my ($self) = @_;
1401
6c553417 1402 my $pgconfdir = "/etc/postgresql/11/main";
86737f12 1403
17424665
DM
1404 my $changes = 0;
1405
1406 $changes = 1 if $self->rewrite_config_file(
1407 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1408
1409 $changes = 1 if $self->rewrite_config_file(
1410 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1411
1412 return $changes;
86737f12
DM
1413}
1414
1415# rewrite /root/.forward
1416sub rewrite_dot_forward {
1417 my ($self) = @_;
1418
c248d69f 1419 my $dstfn = '/root/.forward';
86737f12 1420
0bb9a01a 1421 my $email = $self->get('admin', 'email');
c248d69f 1422
e14fda7a 1423 my $output = '';
86737f12 1424 if ($email && $email =~ m/\s*(\S+)\s*/) {
c248d69f 1425 $output = "$1\n";
86737f12
DM
1426 } else {
1427 # empty .forward does not forward mails (see man local)
1428 }
17424665 1429
c248d69f
DM
1430 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1431
1432 return 0 if defined($old) && ($old eq $output); # no change
1433
1434 PVE::Tools::file_set_contents($dstfn, $output);
1435
1436 return 1;
86737f12
DM
1437}
1438
d15630a9
DM
1439my $write_smtp_whitelist = sub {
1440 my ($filename, $data, $action) = @_;
1441
1442 $action = 'OK' if !$action;
1443
1444 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1445 if -f $filename;
1446
1447 my $new = '';
1448 foreach my $k (sort keys %$data) {
1449 $new .= "$k $action\n";
1450 }
1451
1452 return 0 if defined($old) && ($old eq $new); # no change
1453
1454 PVE::Tools::file_set_contents($filename, $new);
1455
1456 PMG::Utils::run_postmap($filename);
1457
1458 return 1;
1459};
1460
f9967a49 1461sub rewrite_postfix_whitelist {
d15630a9
DM
1462 my ($rulecache) = @_;
1463
1464 # see man page for regexp_table for postfix regex table format
1465
1466 # we use a hash to avoid duplicate entries in regex tables
1467 my $tolist = {};
1468 my $fromlist = {};
1469 my $clientlist = {};
1470
1471 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1472 my $oclass = ref($obj);
1473 if ($oclass eq 'PMG::RuleDB::Receiver') {
1474 my $addr = PMG::Utils::quote_regex($obj->{address});
1475 $tolist->{"/^$addr\$/"} = 1;
1476 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1477 my $addr = PMG::Utils::quote_regex($obj->{address});
1478 $tolist->{"/^.+\@$addr\$/"} = 1;
1479 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1480 my $addr = $obj->{address};
1481 $addr =~ s|/|\\/|g;
1482 $tolist->{"/^$addr\$/"} = 1;
1483 }
1484 }
1485
1486 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1487 my $oclass = ref($obj);
1488 my $addr = PMG::Utils::quote_regex($obj->{address});
1489 if ($oclass eq 'PMG::RuleDB::EMail') {
1490 my $addr = PMG::Utils::quote_regex($obj->{address});
1491 $fromlist->{"/^$addr\$/"} = 1;
1492 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1493 my $addr = PMG::Utils::quote_regex($obj->{address});
1494 $fromlist->{"/^.+\@$addr\$/"} = 1;
1495 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1496 my $addr = $obj->{address};
1497 $addr =~ s|/|\\/|g;
1498 $fromlist->{"/^$addr\$/"} = 1;
1499 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1500 $clientlist->{$obj->{address}} = 1;
1501 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1502 $clientlist->{$obj->{address}} = 1;
1503 }
1504 }
1505
1506 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1507 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1508 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1509 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1510};
1511
f609bf7f
DM
1512# rewrite /etc/postfix/*
1513sub rewrite_config_postfix {
d15630a9 1514 my ($self, $rulecache) = @_;
f609bf7f 1515
3546daf0 1516 # make sure we have required files (else postfix start fails)
3546daf0 1517 IO::File->new($transport_map_filename, 'a', 0644);
f609bf7f 1518
17424665
DM
1519 my $changes = 0;
1520
f609bf7f
DM
1521 if ($self->get('mail', 'tls')) {
1522 eval {
bc44eb02 1523 PMG::Utils::gen_proxmox_tls_cert();
f609bf7f 1524 };
b902c0b8 1525 syslog ('info', "generating certificate failed: $@") if $@;
f609bf7f
DM
1526 }
1527
17424665
DM
1528 $changes = 1 if $self->rewrite_config_file(
1529 'main.cf.in', '/etc/postfix/main.cf');
1530
1531 $changes = 1 if $self->rewrite_config_file(
1532 'master.cf.in', '/etc/postfix/master.cf');
1533
a0d4ce8d
DM
1534 # make sure we have required files (else postfix start fails)
1535 # Note: postmap need a valid /etc/postfix/main.cf configuration
1536 postmap_pmg_domains();
1537 postmap_pmg_transport();
1538 postmap_tls_policy();
1539
f9967a49 1540 rewrite_postfix_whitelist($rulecache) if $rulecache;
d15630a9 1541
f609bf7f
DM
1542 # make sure aliases.db is up to date
1543 system('/usr/bin/newaliases');
17424665
DM
1544
1545 return $changes;
f609bf7f
DM
1546}
1547
f983300f 1548sub rewrite_config {
d15630a9 1549 my ($self, $rulecache, $restart_services, $force_restart) = @_;
c248d69f 1550
798df412
DM
1551 $force_restart = {} if ! $force_restart;
1552
e5bd6522
TL
1553 my $log_restart = sub {
1554 syslog ('info', "configuration change detected for '$_[0]', restarting");
1555 };
1556
d15630a9 1557 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
798df412 1558 $force_restart->{postfix}) {
e5bd6522 1559 $log_restart->('postfix');
2473cb81 1560 PMG::Utils::service_cmd('postfix', 'reload');
c248d69f
DM
1561 }
1562
1563 if ($self->rewrite_dot_forward() && $restart_services) {
1564 # no need to restart anything
1565 }
1566
1567 if ($self->rewrite_config_postgres() && $restart_services) {
1568 # do nothing (too many side effects)?
1569 # does not happen anyways, because config does not change.
1570 }
f983300f 1571
798df412
DM
1572 if (($self->rewrite_config_spam() && $restart_services) ||
1573 $force_restart->{spam}) {
e5bd6522 1574 $log_restart->('pmg-smtp-filter');
c248d69f
DM
1575 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1576 }
1577
798df412
DM
1578 if (($self->rewrite_config_clam() && $restart_services) ||
1579 $force_restart->{clam}) {
e5bd6522 1580 $log_restart->('clamav-daemon');
8f87fe74 1581 PMG::Utils::service_cmd('clamav-daemon', 'restart');
c248d69f
DM
1582 }
1583
798df412
DM
1584 if (($self->rewrite_config_freshclam() && $restart_services) ||
1585 $force_restart->{freshclam}) {
e5bd6522 1586 $log_restart->('clamav-freshclam');
8f87fe74 1587 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
c248d69f 1588 }
f983300f
DM
1589}
1590
7e0e6dbe 15911;