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