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