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