]> git.proxmox.com Git - pmg-api.git/blame - PMG/Config.pm
do not depend on clamav-unofficial-sigs
[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}
7e0e6dbe
DM
593package PMG::Config;
594
595use strict;
596use warnings;
9123cab5 597use IO::File;
7e0e6dbe 598use Data::Dumper;
4ccdc564 599use Template;
7e0e6dbe 600
9123cab5 601use PVE::SafeSyslog;
ba323310 602use PVE::Tools qw($IPV4RE $IPV6RE);
7e0e6dbe 603use PVE::INotify;
b86ac4eb 604use PVE::JSONSchema;
7e0e6dbe 605
ac5d1312 606PMG::Config::Admin->register();
d9dc3c08 607PMG::Config::Mail->register();
fc070a06
DM
608PMG::Config::SpamQuarantine->register();
609PMG::Config::VirusQuarantine->register();
7e0e6dbe 610PMG::Config::Spam->register();
f62194b2 611PMG::Config::ClamAV->register();
7e0e6dbe
DM
612
613# initialize all plugins
614PMG::Config::Base->init();
615
b86ac4eb
DM
616PVE::JSONSchema::register_format(
617 'transport-domain', \&pmg_verify_transport_domain);
618sub pmg_verify_transport_domain {
619 my ($name, $noerr) = @_;
620
621 # like dns-name, but can contain leading dot
622 my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
623
624 if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
625 return undef if $noerr;
626 die "value does not look like a valid transport domain\n";
627 }
628 return $name;
629}
f62194b2
DM
630
631sub new {
632 my ($type) = @_;
633
634 my $class = ref($type) || $type;
635
636 my $cfg = PVE::INotify::read_file("pmg.conf");
637
638 return bless $cfg, $class;
639}
640
be6e2db9
DM
641sub write {
642 my ($self) = @_;
643
644 PVE::INotify::write_file("pmg.conf", $self);
645}
646
f21d933c
DM
647my $lockfile = "/var/lock/pmgconfig.lck";
648
649sub lock_config {
650 my ($code, $errmsg) = @_;
651
652 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
653 if (my $err = $@) {
654 $errmsg ? die "$errmsg: $err" : die $err;
655 }
656}
657
062f0498 658# set section values
062f0498
DM
659sub set {
660 my ($self, $section, $key, $value) = @_;
661
662 my $pdata = PMG::Config::Base->private();
663
062f0498
DM
664 my $plugin = $pdata->{plugins}->{$section};
665 die "no such section '$section'" if !$plugin;
666
062f0498
DM
667 if (defined($value)) {
668 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
d79b9b0c
DM
669 $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
670 $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
062f0498 671 } else {
d79b9b0c
DM
672 if (defined($self->{ids}->{$section})) {
673 delete $self->{ids}->{$section}->{$key};
062f0498
DM
674 }
675 }
676
677 return undef;
678}
679
f62194b2 680# get section value or default
f62194b2 681sub get {
11081cf6 682 my ($self, $section, $key, $nodefault) = @_;
f62194b2
DM
683
684 my $pdata = PMG::Config::Base->private();
f62194b2 685 my $pdesc = $pdata->{propertyList}->{$key};
3d9837d9
DM
686 die "no such property '$section/$key'\n"
687 if !(defined($pdesc) && defined($pdata->{options}->{$section}) &&
688 defined($pdata->{options}->{$section}->{$key}));
f62194b2 689
d79b9b0c
DM
690 if (defined($self->{ids}->{$section}) &&
691 defined(my $value = $self->{ids}->{$section}->{$key})) {
f62194b2 692 return $value;
1ccc8e95 693 }
f62194b2 694
11081cf6
DM
695 return undef if $nodefault;
696
f62194b2
DM
697 return $pdesc->{default};
698}
699
1ccc8e95 700# get a whole section with default value
1ccc8e95
DM
701sub get_section {
702 my ($self, $section) = @_;
703
704 my $pdata = PMG::Config::Base->private();
705 return undef if !defined($pdata->{options}->{$section});
706
707 my $res = {};
708
709 foreach my $key (keys %{$pdata->{options}->{$section}}) {
710
711 my $pdesc = $pdata->{propertyList}->{$key};
712
d79b9b0c
DM
713 if (defined($self->{ids}->{$section}) &&
714 defined(my $value = $self->{ids}->{$section}->{$key})) {
1ccc8e95
DM
715 $res->{$key} = $value;
716 next;
717 }
718 $res->{$key} = $pdesc->{default};
719 }
720
721 return $res;
722}
723
be16be07 724# get a whole config with default values
be16be07
DM
725sub get_config {
726 my ($self) = @_;
727
9dab5fe5
DM
728 my $pdata = PMG::Config::Base->private();
729
be16be07
DM
730 my $res = {};
731
9dab5fe5 732 foreach my $type (keys %{$pdata->{plugins}}) {
9dab5fe5
DM
733 my $plugin = $pdata->{plugins}->{$type};
734 $res->{$type} = $self->get_section($type);
be16be07
DM
735 }
736
737 return $res;
738}
739
7e0e6dbe
DM
740sub read_pmg_conf {
741 my ($filename, $fh) = @_;
f62194b2 742
7e0e6dbe 743 local $/ = undef; # slurp mode
f62194b2 744
9dfe7c16 745 my $raw = <$fh> if defined($fh);
7e0e6dbe
DM
746
747 return PMG::Config::Base->parse_config($filename, $raw);
748}
749
750sub write_pmg_conf {
751 my ($filename, $fh, $cfg) = @_;
752
753 my $raw = PMG::Config::Base->write_config($filename, $cfg);
754
755 PVE::Tools::safe_print($filename, $fh, $raw);
756}
757
3278b571 758PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
f62194b2 759 \&read_pmg_conf,
9dfe7c16
DM
760 \&write_pmg_conf,
761 undef, always_call_parser => 1);
7e0e6dbe 762
f609bf7f
DM
763# parsers/writers for other files
764
3278b571 765my $domainsfilename = "/etc/pmg/domains";
f609bf7f 766
c3f4336c
DM
767sub postmap_pmg_domains {
768 PMG::Utils::run_postmap($domainsfilename);
769}
770
f609bf7f
DM
771sub read_pmg_domains {
772 my ($filename, $fh) = @_;
773
b7298186 774 my $domains = {};
f609bf7f 775
b7298186 776 my $comment = '';
f609bf7f
DM
777 if (defined($fh)) {
778 while (defined(my $line = <$fh>)) {
3118b703
DM
779 chomp $line;
780 next if $line =~ m/^\s*$/;
b7298186
DM
781 if ($line =~ m/^#(.*)\s*$/) {
782 $comment = $1;
783 next;
784 }
785 if ($line =~ m/^(\S+)\s.*$/) {
f609bf7f 786 my $domain = $1;
b7298186
DM
787 $domains->{$domain} = {
788 domain => $domain, comment => $comment };
789 $comment = '';
3118b703
DM
790 } else {
791 warn "parse error in '$filename': $line\n";
792 $comment = '';
f609bf7f
DM
793 }
794 }
795 }
796
797 return $domains;
798}
799
800sub write_pmg_domains {
b7298186
DM
801 my ($filename, $fh, $domains) = @_;
802
803 foreach my $domain (sort keys %$domains) {
804 my $comment = $domains->{$domain}->{comment};
805 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
806 if defined($comment) && $comment !~ m/^\s*$/;
f609bf7f 807
6b31da64 808 PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
f609bf7f
DM
809 }
810}
811
812PVE::INotify::register_file('domains', $domainsfilename,
813 \&read_pmg_domains,
814 \&write_pmg_domains,
815 undef, always_call_parser => 1);
816
bef31f06
DM
817my $mynetworks_filename = "/etc/pmg/mynetworks";
818
819sub postmap_pmg_mynetworks {
820 PMG::Utils::run_postmap($mynetworks_filename);
821}
822
823sub read_pmg_mynetworks {
824 my ($filename, $fh) = @_;
825
826 my $mynetworks = {};
827
828 my $comment = '';
829 if (defined($fh)) {
830 while (defined(my $line = <$fh>)) {
831 chomp $line;
832 next if $line =~ m/^\s*$/;
833 if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
834 my ($network, $prefix_size, $comment) = ($1, $2, $3);
835 my $cidr = "$network/${prefix_size}";
836 $mynetworks->{$cidr} = {
837 cidr => $cidr,
838 network_address => $network,
839 prefix_size => $prefix_size,
840 comment => $comment // '',
841 };
842 } else {
843 warn "parse error in '$filename': $line\n";
844 }
845 }
846 }
847
848 return $mynetworks;
849}
850
851sub write_pmg_mynetworks {
852 my ($filename, $fh, $mynetworks) = @_;
853
854 foreach my $cidr (sort keys %$mynetworks) {
855 my $data = $mynetworks->{$cidr};
856 my $comment = $data->{comment} // '*';
857 PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
858 }
859}
860
861PVE::INotify::register_file('mynetworks', $mynetworks_filename,
862 \&read_pmg_mynetworks,
863 \&write_pmg_mynetworks,
864 undef, always_call_parser => 1);
865
cd533938 866my $transport_map_filename = "/etc/pmg/transport";
3546daf0 867
3118b703
DM
868sub postmap_pmg_transport {
869 PMG::Utils::run_postmap($transport_map_filename);
870}
871
3546daf0
DM
872sub read_transport_map {
873 my ($filename, $fh) = @_;
874
875 return [] if !defined($fh);
876
877 my $res = {};
878
3118b703 879 my $comment = '';
b7c49fec 880
3546daf0
DM
881 while (defined(my $line = <$fh>)) {
882 chomp $line;
883 next if $line =~ m/^\s*$/;
3118b703
DM
884 if ($line =~ m/^#(.*)\s*$/) {
885 $comment = $1;
886 next;
887 }
3546daf0 888
b7c49fec
DM
889 my $parse_error = sub {
890 my ($err) = @_;
891 warn "parse error in '$filename': $line - $err";
892 $comment = '';
893 };
894
ba323310 895 if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
3118b703 896 my ($domain, $host, $port) = ($1, $2, $3);
3546daf0 897
b7c49fec
DM
898 eval { pmg_verify_transport_domain($domain); };
899 if (my $err = $@) {
900 $parse_error->($err);
901 next;
902 }
53904163 903 my $use_mx = 1;
3546daf0
DM
904 if ($host =~ m/^\[(.*)\]$/) {
905 $host = $1;
53904163 906 $use_mx = 0;
3546daf0
DM
907 }
908
b7c49fec
DM
909 eval { PVE::JSONSchema::pve_verify_address($host); };
910 if (my $err = $@) {
911 $parse_error->($err);
912 next;
913 }
914
3118b703
DM
915 my $data = {
916 domain => $domain,
917 host => $host,
918 port => $port,
53904163 919 use_mx => $use_mx,
3118b703
DM
920 comment => $comment,
921 };
922 $res->{$domain} = $data;
923 $comment = '';
924 } else {
b7c49fec 925 $parse_error->('wrong format');
3546daf0
DM
926 }
927 }
928
3118b703 929 return $res;
3546daf0
DM
930}
931
cd533938 932sub write_transport_map {
3546daf0
DM
933 my ($filename, $fh, $tmap) = @_;
934
935 return if !$tmap;
936
3118b703
DM
937 foreach my $domain (sort keys %$tmap) {
938 my $data = $tmap->{$domain};
3546daf0 939
3118b703
DM
940 my $comment = $data->{comment};
941 PVE::Tools::safe_print($filename, $fh, "#$comment\n")
942 if defined($comment) && $comment !~ m/^\s*$/;
943
ba323310
DM
944 my $use_mx = $data->{use_mx};
945 $use_mx = 0 if $data->{host} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
946
947 if ($use_mx) {
3118b703 948 PVE::Tools::safe_print(
53904163 949 $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
3118b703
DM
950 } else {
951 PVE::Tools::safe_print(
53904163 952 $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
3546daf0
DM
953 }
954 }
955}
956
957PVE::INotify::register_file('transport', $transport_map_filename,
958 \&read_transport_map,
cd533938 959 \&write_transport_map,
3546daf0 960 undef, always_call_parser => 1);
7e0e6dbe 961
4ccdc564
DM
962# config file generation using templates
963
07b3face
DM
964sub get_template_vars {
965 my ($self) = @_;
4ccdc564
DM
966
967 my $vars = { pmg => $self->get_config() };
968
f609bf7f
DM
969 my $nodename = PVE::INotify::nodename();
970 my $int_ip = PMG::Cluster::remote_node_ip($nodename);
971 my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip);
f609bf7f
DM
972 $vars->{ipconfig}->{int_ip} = $int_ip;
973 # $vars->{ipconfig}->{int_net_cidr} = $int_net_cidr;
f609bf7f 974
ba323310
DM
975 my $transportnets = [];
976
977 my $tmap = PVE::INotify::read_file('transport');
978 foreach my $domain (sort keys %$tmap) {
979 my $data = $tmap->{$domain};
980 my $host = $data->{host};
981 if ($host =~ m/^$IPV4RE$/) {
982 push @$transportnets, "$host/32";
983 } elsif ($host =~ m/^$IPV6RE$/) {
984 push @$transportnets, "[$host]/128";
985 }
986 }
987
f609bf7f
DM
988 $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
989
990 my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
991 push @$mynetworks, @$transportnets;
992 push @$mynetworks, $int_net_cidr;
bef31f06 993 push @$mynetworks, 'hash:/etc/pmg/mynetworks';
f609bf7f 994
bef31f06 995 my $netlist = PVE::INotify::read_file('mynetworks');
f609bf7f
DM
996 # add default relay to mynetworks
997 if (my $relay = $self->get('mail', 'relay')) {
ba323310 998 if ($relay =~ m/^$IPV4RE$/) {
f609bf7f 999 push @$mynetworks, "$relay/32";
ba323310 1000 } elsif ($relay =~ m/^$IPV6RE$/) {
f609bf7f
DM
1001 push @$mynetworks, "[$relay]/128";
1002 } else {
66af5153 1003 # DNS name - do nothing ?
f609bf7f
DM
1004 }
1005 }
1006
1007 $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
1008
1009 my $usepolicy = 0;
1010 $usepolicy = 1 if $self->get('mail', 'greylist') ||
1011 $self->get('mail', 'spf') || $self->get('mail', 'use_rbl');
1012 $vars->{postfix}->{usepolicy} = $usepolicy;
1013
1014 my $resolv = PVE::INotify::read_file('resolvconf');
1015 $vars->{dns}->{hostname} = $nodename;
1016 $vars->{dns}->{domain} = $resolv->{search};
1017
11081cf6
DM
1018 if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
1019 eval {
1020 my $uri = URI->new($proxy);
1021 my $host = $uri->host;
1022 my $port = $uri->port // 8080;
1023 if ($host) {
1024 my $data = { host => $host, port => $port };
1025 if (my $ui = $uri->userinfo) {
1026 my ($username, $pw) = split(/:/, $ui, 2);
1027 $data->{username} = $username;
1028 $data->{password} = $pw if defined($pw);
1029 }
1030 $vars->{proxy} = $data;
1031 }
1032 };
1033 warn "parse http_proxy failed - $@" if $@;
1034 }
1035
07b3face
DM
1036 return $vars;
1037}
1038
310daf18
DM
1039# use one global TT cache
1040our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
1041
1042my $template_toolkit;
1043
1044sub get_template_toolkit {
1045
1046 return $template_toolkit if $template_toolkit;
1047
1048 $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
1049
1050 return $template_toolkit;
1051}
1052
07b3face
DM
1053# rewrite file from template
1054# return true if file has changed
1055sub rewrite_config_file {
1056 my ($self, $tmplname, $dstfn) = @_;
1057
1058 my $demo = $self->get('admin', 'demo');
1059
07b3face 1060 if ($demo) {
310daf18
DM
1061 my $demosrc = "$tmplname.demo";
1062 $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
07b3face
DM
1063 }
1064
c248d69f 1065 my ($perm, $uid, $gid);
07b3face 1066
07b3face
DM
1067 if ($dstfn eq '/etc/fetchmailrc') {
1068 (undef, undef, $uid, $gid) = getpwnam('fetchmail');
1069 $perm = 0600;
1070 } elsif ($dstfn eq '/etc/clamav/freshclam.conf') {
1071 # needed if file contains a HTTPProxyPasswort
1072
1073 $uid = getpwnam('clamav');
1074 $gid = getgrnam('adm');
1075 $perm = 0600;
1076 }
1077
310daf18 1078 my $tt = get_template_toolkit();
07b3face
DM
1079
1080 my $vars = $self->get_template_vars();
1081
c248d69f 1082 my $output = '';
07b3face 1083
310daf18 1084 $tt->process($tmplname, $vars, \$output) ||
60f82a46 1085 die $tt->error() . "\n";
07b3face
DM
1086
1087 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1088
1089 return 0 if defined($old) && ($old eq $output); # no change
1090
1091 PVE::Tools::file_set_contents($dstfn, $output, $perm);
1092
1093 if (defined($uid) && defined($gid)) {
1094 chown($uid, $gid, $dstfn);
1095 }
1096
1097 return 1;
4ccdc564
DM
1098}
1099
9123cab5
DM
1100# rewrite spam configuration
1101sub rewrite_config_spam {
1102 my ($self) = @_;
1103
1104 my $use_awl = $self->get('spam', 'use_awl');
1105 my $use_bayes = $self->get('spam', 'use_bayes');
1106 my $use_razor = $self->get('spam', 'use_razor');
1107
17424665
DM
1108 my $changes = 0;
1109
9123cab5 1110 # delete AW and bayes databases if those features are disabled
17424665
DM
1111 if (!$use_awl) {
1112 $changes = 1 if unlink '/root/.spamassassin/auto-whitelist';
1113 }
1114
9123cab5 1115 if (!$use_bayes) {
17424665
DM
1116 $changes = 1 if unlink '/root/.spamassassin/bayes_journal';
1117 $changes = 1 if unlink '/root/.spamassassin/bayes_seen';
1118 $changes = 1 if unlink '/root/.spamassassin/bayes_toks';
9123cab5
DM
1119 }
1120
1121 # make sure we have a custom.cf file (else cluster sync fails)
1122 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
1123
17424665
DM
1124 $changes = 1 if $self->rewrite_config_file(
1125 'local.cf.in', '/etc/mail/spamassassin/local.cf');
1126
1127 $changes = 1 if $self->rewrite_config_file(
1128 'init.pre.in', '/etc/mail/spamassassin/init.pre');
1129
1130 $changes = 1 if $self->rewrite_config_file(
1131 'v310.pre.in', '/etc/mail/spamassassin/v310.pre');
1132
1133 $changes = 1 if $self->rewrite_config_file(
1134 'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
9123cab5
DM
1135
1136 if ($use_razor) {
1137 mkdir "/root/.razor";
17424665
DM
1138
1139 $changes = 1 if $self->rewrite_config_file(
1140 'razor-agent.conf.in', '/root/.razor/razor-agent.conf');
1141
9123cab5
DM
1142 if (! -e '/root/.razor/identity') {
1143 eval {
1144 my $timeout = 30;
17424665
DM
1145 PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout);
1146 PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
9123cab5
DM
1147 };
1148 my $err = $@;
b902c0b8 1149 syslog('info', "registering razor failed: $err") if $err;
9123cab5
DM
1150 }
1151 }
17424665
DM
1152
1153 return $changes;
9123cab5
DM
1154}
1155
ac5d1312
DM
1156# rewrite ClamAV configuration
1157sub rewrite_config_clam {
1158 my ($self) = @_;
1159
17424665
DM
1160 return $self->rewrite_config_file(
1161 'clamd.conf.in', '/etc/clamav/clamd.conf');
1162}
1163
1164sub rewrite_config_freshclam {
1165 my ($self) = @_;
1166
1167 return $self->rewrite_config_file(
1168 'freshclam.conf.in', '/etc/clamav/freshclam.conf');
ac5d1312
DM
1169}
1170
86737f12
DM
1171sub rewrite_config_postgres {
1172 my ($self) = @_;
1173
1174 my $pgconfdir = "/etc/postgresql/9.6/main";
1175
17424665
DM
1176 my $changes = 0;
1177
1178 $changes = 1 if $self->rewrite_config_file(
1179 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
1180
1181 $changes = 1 if $self->rewrite_config_file(
1182 'postgresql.conf.in', "$pgconfdir/postgresql.conf");
1183
1184 return $changes;
86737f12
DM
1185}
1186
1187# rewrite /root/.forward
1188sub rewrite_dot_forward {
1189 my ($self) = @_;
1190
c248d69f 1191 my $dstfn = '/root/.forward';
86737f12 1192
0bb9a01a 1193 my $email = $self->get('admin', 'email');
c248d69f 1194
e14fda7a 1195 my $output = '';
86737f12 1196 if ($email && $email =~ m/\s*(\S+)\s*/) {
c248d69f 1197 $output = "$1\n";
86737f12
DM
1198 } else {
1199 # empty .forward does not forward mails (see man local)
1200 }
17424665 1201
c248d69f
DM
1202 my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
1203
1204 return 0 if defined($old) && ($old eq $output); # no change
1205
1206 PVE::Tools::file_set_contents($dstfn, $output);
1207
1208 return 1;
86737f12
DM
1209}
1210
d15630a9
DM
1211my $write_smtp_whitelist = sub {
1212 my ($filename, $data, $action) = @_;
1213
1214 $action = 'OK' if !$action;
1215
1216 my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
1217 if -f $filename;
1218
1219 my $new = '';
1220 foreach my $k (sort keys %$data) {
1221 $new .= "$k $action\n";
1222 }
1223
1224 return 0 if defined($old) && ($old eq $new); # no change
1225
1226 PVE::Tools::file_set_contents($filename, $new);
1227
1228 PMG::Utils::run_postmap($filename);
1229
1230 return 1;
1231};
1232
1233my $rewrite_config_whitelist = sub {
1234 my ($rulecache) = @_;
1235
1236 # see man page for regexp_table for postfix regex table format
1237
1238 # we use a hash to avoid duplicate entries in regex tables
1239 my $tolist = {};
1240 my $fromlist = {};
1241 my $clientlist = {};
1242
1243 foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
1244 my $oclass = ref($obj);
1245 if ($oclass eq 'PMG::RuleDB::Receiver') {
1246 my $addr = PMG::Utils::quote_regex($obj->{address});
1247 $tolist->{"/^$addr\$/"} = 1;
1248 } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
1249 my $addr = PMG::Utils::quote_regex($obj->{address});
1250 $tolist->{"/^.+\@$addr\$/"} = 1;
1251 } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
1252 my $addr = $obj->{address};
1253 $addr =~ s|/|\\/|g;
1254 $tolist->{"/^$addr\$/"} = 1;
1255 }
1256 }
1257
1258 foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
1259 my $oclass = ref($obj);
1260 my $addr = PMG::Utils::quote_regex($obj->{address});
1261 if ($oclass eq 'PMG::RuleDB::EMail') {
1262 my $addr = PMG::Utils::quote_regex($obj->{address});
1263 $fromlist->{"/^$addr\$/"} = 1;
1264 } elsif ($oclass eq 'PMG::RuleDB::Domain') {
1265 my $addr = PMG::Utils::quote_regex($obj->{address});
1266 $fromlist->{"/^.+\@$addr\$/"} = 1;
1267 } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
1268 my $addr = $obj->{address};
1269 $addr =~ s|/|\\/|g;
1270 $fromlist->{"/^$addr\$/"} = 1;
1271 } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
1272 $clientlist->{$obj->{address}} = 1;
1273 } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
1274 $clientlist->{$obj->{address}} = 1;
1275 }
1276 }
1277
1278 $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
1279 $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
1280 $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
1281 $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
1282};
1283
f609bf7f
DM
1284# rewrite /etc/postfix/*
1285sub rewrite_config_postfix {
d15630a9 1286 my ($self, $rulecache) = @_;
f609bf7f 1287
3546daf0 1288 # make sure we have required files (else postfix start fails)
b7298186 1289 postmap_pmg_domains();
ba323310 1290 postmap_pmg_transport();
bef31f06 1291 postmap_pmg_mynetworks();
b7298186 1292
3546daf0 1293 IO::File->new($transport_map_filename, 'a', 0644);
f609bf7f 1294
17424665
DM
1295 my $changes = 0;
1296
f609bf7f
DM
1297 if ($self->get('mail', 'tls')) {
1298 eval {
bc44eb02 1299 PMG::Utils::gen_proxmox_tls_cert();
f609bf7f 1300 };
b902c0b8 1301 syslog ('info', "generating certificate failed: $@") if $@;
f609bf7f
DM
1302 }
1303
17424665
DM
1304 $changes = 1 if $self->rewrite_config_file(
1305 'main.cf.in', '/etc/postfix/main.cf');
1306
1307 $changes = 1 if $self->rewrite_config_file(
1308 'master.cf.in', '/etc/postfix/master.cf');
1309
c90f3170 1310 $rewrite_config_whitelist->($rulecache) if $rulecache;
d15630a9
DM
1311
1312 # fixme: rewrite_config_tls_policy ($class);
f609bf7f
DM
1313
1314 # make sure aliases.db is up to date
1315 system('/usr/bin/newaliases');
17424665
DM
1316
1317 return $changes;
f609bf7f
DM
1318}
1319
f983300f 1320sub rewrite_config {
d15630a9 1321 my ($self, $rulecache, $restart_services, $force_restart) = @_;
c248d69f 1322
798df412
DM
1323 $force_restart = {} if ! $force_restart;
1324
d15630a9 1325 if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
798df412 1326 $force_restart->{postfix}) {
c248d69f
DM
1327 PMG::Utils::service_cmd('postfix', 'restart');
1328 }
1329
1330 if ($self->rewrite_dot_forward() && $restart_services) {
1331 # no need to restart anything
1332 }
1333
1334 if ($self->rewrite_config_postgres() && $restart_services) {
1335 # do nothing (too many side effects)?
1336 # does not happen anyways, because config does not change.
1337 }
f983300f 1338
798df412
DM
1339 if (($self->rewrite_config_spam() && $restart_services) ||
1340 $force_restart->{spam}) {
c248d69f
DM
1341 PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
1342 }
1343
798df412
DM
1344 if (($self->rewrite_config_clam() && $restart_services) ||
1345 $force_restart->{clam}) {
8f87fe74 1346 PMG::Utils::service_cmd('clamav-daemon', 'restart');
c248d69f
DM
1347 }
1348
798df412
DM
1349 if (($self->rewrite_config_freshclam() && $restart_services) ||
1350 $force_restart->{freshclam}) {
8f87fe74 1351 PMG::Utils::service_cmd('clamav-freshclam', 'restart');
c248d69f 1352 }
f983300f
DM
1353}
1354
7e0e6dbe 13551;