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