]>
Commit | Line | Data |
---|---|---|
1 | package PMG::Config::Base; | |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use URI; | |
6 | use Data::Dumper; | |
7 | ||
8 | use PVE::Tools; | |
9 | use PVE::JSONSchema qw(get_standard_option); | |
10 | use PVE::SectionConfig; | |
11 | ||
12 | use base qw(PVE::SectionConfig); | |
13 | ||
14 | my $defaultData = { | |
15 | propertyList => { | |
16 | type => { description => "Section type." }, | |
17 | section => { | |
18 | description => "Section ID.", | |
19 | type => 'string', format => 'pve-configid', | |
20 | }, | |
21 | }, | |
22 | }; | |
23 | ||
24 | sub private { | |
25 | return $defaultData; | |
26 | } | |
27 | ||
28 | sub format_section_header { | |
29 | my ($class, $type, $sectionId) = @_; | |
30 | ||
31 | die "internal error ($type ne $sectionId)" if $type ne $sectionId; | |
32 | ||
33 | return "section: $type\n"; | |
34 | } | |
35 | ||
36 | ||
37 | sub parse_section_header { | |
38 | my ($class, $line) = @_; | |
39 | ||
40 | if ($line =~ m/^section:\s*(\S+)\s*$/) { | |
41 | my $section = $1; | |
42 | my $errmsg = undef; # set if you want to skip whole section | |
43 | eval { PVE::JSONSchema::pve_verify_configid($section); }; | |
44 | $errmsg = $@ if $@; | |
45 | my $config = {}; # to return additional attributes | |
46 | return ($section, $section, $errmsg, $config); | |
47 | } | |
48 | return undef; | |
49 | } | |
50 | ||
51 | package PMG::Config::Admin; | |
52 | ||
53 | use strict; | |
54 | use warnings; | |
55 | ||
56 | use base qw(PMG::Config::Base); | |
57 | ||
58 | sub type { | |
59 | return 'admin'; | |
60 | } | |
61 | ||
62 | sub properties { | |
63 | return { | |
64 | advfilter => { | |
65 | description => "Use advanced filters for statistic.", | |
66 | type => 'boolean', | |
67 | default => 1, | |
68 | }, | |
69 | dailyreport => { | |
70 | description => "Send daily reports.", | |
71 | type => 'boolean', | |
72 | default => 1, | |
73 | }, | |
74 | statlifetime => { | |
75 | description => "User Statistics Lifetime (days)", | |
76 | type => 'integer', | |
77 | default => 7, | |
78 | minimum => 1, | |
79 | }, | |
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', | |
89 | }, | |
90 | http_proxy => { | |
91 | description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')", | |
92 | type => 'string', | |
93 | pattern => "http://.*", | |
94 | }, | |
95 | avast => { | |
96 | description => "Use Avast Virus Scanner (/usr/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.", | |
97 | type => 'boolean', | |
98 | default => 0, | |
99 | }, | |
100 | clamav => { | |
101 | description => "Use ClamAV Virus Scanner. This is the default virus scanner and is enabled by default.", | |
102 | type => 'boolean', | |
103 | default => 1, | |
104 | }, | |
105 | custom_check => { | |
106 | description => "Use Custom Check Script. The script has to take the defined arguments and can return Virus findings or a Spamscore.", | |
107 | type => 'boolean', | |
108 | default => 0, | |
109 | }, | |
110 | custom_check_path => { | |
111 | description => "Absolute Path to the Custom Check Script", | |
112 | type => 'string', pattern => '^/([^/\0]+\/)+[^/\0]+$', | |
113 | default => '/usr/local/bin/pmg-custom-check', | |
114 | }, | |
115 | dkim_sign => { | |
116 | description => "DKIM sign outbound mails with the configured Selector.", | |
117 | type => 'boolean', | |
118 | default => 0, | |
119 | }, | |
120 | dkim_sign_all_mail => { | |
121 | description => "DKIM sign all outgoing mails irrespective of the Envelope From domain.", | |
122 | type => 'boolean', | |
123 | default => 0, | |
124 | }, | |
125 | dkim_selector => { | |
126 | description => "Default DKIM selector", | |
127 | type => 'string', format => 'dns-name', #see RFC6376 3.1 | |
128 | }, | |
129 | }; | |
130 | } | |
131 | ||
132 | sub options { | |
133 | return { | |
134 | advfilter => { optional => 1 }, | |
135 | avast => { optional => 1 }, | |
136 | clamav => { optional => 1 }, | |
137 | statlifetime => { optional => 1 }, | |
138 | dailyreport => { optional => 1 }, | |
139 | demo => { optional => 1 }, | |
140 | email => { optional => 1 }, | |
141 | http_proxy => { optional => 1 }, | |
142 | custom_check => { optional => 1 }, | |
143 | custom_check_path => { optional => 1 }, | |
144 | dkim_sign => { optional => 1 }, | |
145 | dkim_sign_all_mail => { optional => 1 }, | |
146 | dkim_selector => { optional => 1 }, | |
147 | }; | |
148 | } | |
149 | ||
150 | package PMG::Config::Spam; | |
151 | ||
152 | use strict; | |
153 | use warnings; | |
154 | ||
155 | use base qw(PMG::Config::Base); | |
156 | ||
157 | sub type { | |
158 | return 'spam'; | |
159 | } | |
160 | ||
161 | sub properties { | |
162 | return { | |
163 | languages => { | |
164 | description => "This option is used to specify which languages are considered OK for incoming mail.", | |
165 | type => 'string', | |
166 | pattern => '(all|([a-z][a-z])+( ([a-z][a-z])+)*)', | |
167 | default => 'all', | |
168 | }, | |
169 | use_bayes => { | |
170 | description => "Whether to use the naive-Bayesian-style classifier.", | |
171 | type => 'boolean', | |
172 | default => 1, | |
173 | }, | |
174 | use_awl => { | |
175 | description => "Use the Auto-Whitelist plugin.", | |
176 | type => 'boolean', | |
177 | default => 1, | |
178 | }, | |
179 | use_razor => { | |
180 | description => "Whether to use Razor2, if it is available.", | |
181 | type => 'boolean', | |
182 | default => 1, | |
183 | }, | |
184 | wl_bounce_relays => { | |
185 | description => "Whitelist legitimate bounce relays.", | |
186 | type => 'string', | |
187 | }, | |
188 | clamav_heuristic_score => { | |
189 | description => "Score for ClamAV heuristics (Encrypted Archives/Documents, Google Safe Browsing database, PhishingScanURLs, ...).", | |
190 | type => 'integer', | |
191 | minimum => 0, | |
192 | maximum => 1000, | |
193 | default => 3, | |
194 | }, | |
195 | bounce_score => { | |
196 | description => "Additional score for bounce mails.", | |
197 | type => 'integer', | |
198 | minimum => 0, | |
199 | maximum => 1000, | |
200 | default => 0, | |
201 | }, | |
202 | rbl_checks => { | |
203 | description => "Enable real time blacklists (RBL) checks.", | |
204 | type => 'boolean', | |
205 | default => 1, | |
206 | }, | |
207 | maxspamsize => { | |
208 | description => "Maximum size of spam messages in bytes.", | |
209 | type => 'integer', | |
210 | minimum => 64, | |
211 | default => 256*1024, | |
212 | }, | |
213 | }; | |
214 | } | |
215 | ||
216 | sub options { | |
217 | return { | |
218 | use_awl => { optional => 1 }, | |
219 | use_razor => { optional => 1 }, | |
220 | wl_bounce_relays => { optional => 1 }, | |
221 | languages => { optional => 1 }, | |
222 | use_bayes => { optional => 1 }, | |
223 | clamav_heuristic_score => { optional => 1 }, | |
224 | bounce_score => { optional => 1 }, | |
225 | rbl_checks => { optional => 1 }, | |
226 | maxspamsize => { optional => 1 }, | |
227 | }; | |
228 | } | |
229 | ||
230 | package PMG::Config::SpamQuarantine; | |
231 | ||
232 | use strict; | |
233 | use warnings; | |
234 | ||
235 | use base qw(PMG::Config::Base); | |
236 | ||
237 | sub type { | |
238 | return 'spamquar'; | |
239 | } | |
240 | ||
241 | sub properties { | |
242 | return { | |
243 | lifetime => { | |
244 | description => "Quarantine life time (days)", | |
245 | type => 'integer', | |
246 | minimum => 1, | |
247 | default => 7, | |
248 | }, | |
249 | authmode => { | |
250 | 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.", | |
251 | type => 'string', | |
252 | enum => [qw(ticket ldap ldapticket)], | |
253 | default => 'ticket', | |
254 | }, | |
255 | reportstyle => { | |
256 | description => "Spam report style.", | |
257 | type => 'string', | |
258 | enum => [qw(none short verbose custom)], | |
259 | default => 'verbose', | |
260 | }, | |
261 | viewimages => { | |
262 | description => "Allow to view images.", | |
263 | type => 'boolean', | |
264 | default => 1, | |
265 | }, | |
266 | allowhrefs => { | |
267 | description => "Allow to view hyperlinks.", | |
268 | type => 'boolean', | |
269 | default => 1, | |
270 | }, | |
271 | hostname => { | |
272 | description => "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.", | |
273 | type => 'string', format => 'address', | |
274 | }, | |
275 | port => { | |
276 | description => "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.", | |
277 | type => 'integer', | |
278 | minimum => 1, | |
279 | maximum => 65535, | |
280 | default => 8006, | |
281 | }, | |
282 | protocol => { | |
283 | description => "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.", | |
284 | type => 'string', | |
285 | enum => [qw(http https)], | |
286 | default => 'https', | |
287 | }, | |
288 | mailfrom => { | |
289 | description => "Text for 'From' header in daily spam report mails.", | |
290 | type => 'string', | |
291 | }, | |
292 | quarantinelink => { | |
293 | description => "Enables user self-service for Quarantine Links. Caution: this is accessible without authentication", | |
294 | type => 'boolean', | |
295 | default => 0, | |
296 | }, | |
297 | }; | |
298 | } | |
299 | ||
300 | sub options { | |
301 | return { | |
302 | mailfrom => { optional => 1 }, | |
303 | hostname => { optional => 1 }, | |
304 | lifetime => { optional => 1 }, | |
305 | authmode => { optional => 1 }, | |
306 | reportstyle => { optional => 1 }, | |
307 | viewimages => { optional => 1 }, | |
308 | allowhrefs => { optional => 1 }, | |
309 | port => { optional => 1 }, | |
310 | protocol => { optional => 1 }, | |
311 | quarantinelink => { optional => 1 }, | |
312 | }; | |
313 | } | |
314 | ||
315 | package PMG::Config::VirusQuarantine; | |
316 | ||
317 | use strict; | |
318 | use warnings; | |
319 | ||
320 | use base qw(PMG::Config::Base); | |
321 | ||
322 | sub type { | |
323 | return 'virusquar'; | |
324 | } | |
325 | ||
326 | sub properties { | |
327 | return {}; | |
328 | } | |
329 | ||
330 | sub options { | |
331 | return { | |
332 | lifetime => { optional => 1 }, | |
333 | viewimages => { optional => 1 }, | |
334 | allowhrefs => { optional => 1 }, | |
335 | }; | |
336 | } | |
337 | ||
338 | package PMG::Config::ClamAV; | |
339 | ||
340 | use strict; | |
341 | use warnings; | |
342 | ||
343 | use base qw(PMG::Config::Base); | |
344 | ||
345 | sub type { | |
346 | return 'clamav'; | |
347 | } | |
348 | ||
349 | sub properties { | |
350 | return { | |
351 | dbmirror => { | |
352 | description => "ClamAV database mirror server.", | |
353 | type => 'string', | |
354 | default => 'database.clamav.net', | |
355 | }, | |
356 | archiveblockencrypted => { | |
357 | description => "Whether to mark encrypted archives and documents as heuristic virus match. A match does not necessarily result in an immediate block, it just raises the Spam Score by 'clamav_heuristic_score'.", | |
358 | type => 'boolean', | |
359 | default => 0, | |
360 | }, | |
361 | archivemaxrec => { | |
362 | 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.", | |
363 | type => 'integer', | |
364 | minimum => 1, | |
365 | default => 5, | |
366 | }, | |
367 | archivemaxfiles => { | |
368 | 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.", | |
369 | type => 'integer', | |
370 | minimum => 0, | |
371 | default => 1000, | |
372 | }, | |
373 | archivemaxsize => { | |
374 | description => "Files larger than this limit (in bytes) won't be scanned.", | |
375 | type => 'integer', | |
376 | minimum => 1000000, | |
377 | default => 25000000, | |
378 | }, | |
379 | maxscansize => { | |
380 | description => "Sets the maximum amount of data (in bytes) to be scanned for each input file.", | |
381 | type => 'integer', | |
382 | minimum => 1000000, | |
383 | default => 100000000, | |
384 | }, | |
385 | maxcccount => { | |
386 | description => "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.", | |
387 | type => 'integer', | |
388 | minimum => 0, | |
389 | default => 0, | |
390 | }, | |
391 | safebrowsing => { | |
392 | description => "Enables support for Google Safe Browsing.", | |
393 | type => 'boolean', | |
394 | default => 1 | |
395 | }, | |
396 | scriptedupdates => { | |
397 | description => "Enables ScriptedUpdates (incremental download of signatures)", | |
398 | type => 'boolean', | |
399 | default => 0 | |
400 | }, | |
401 | }; | |
402 | } | |
403 | ||
404 | sub options { | |
405 | return { | |
406 | archiveblockencrypted => { optional => 1 }, | |
407 | archivemaxrec => { optional => 1 }, | |
408 | archivemaxfiles => { optional => 1 }, | |
409 | archivemaxsize => { optional => 1 }, | |
410 | maxscansize => { optional => 1 }, | |
411 | dbmirror => { optional => 1 }, | |
412 | maxcccount => { optional => 1 }, | |
413 | safebrowsing => { optional => 1 }, | |
414 | scriptedupdates => { optional => 1}, | |
415 | }; | |
416 | } | |
417 | ||
418 | package PMG::Config::Mail; | |
419 | ||
420 | use strict; | |
421 | use warnings; | |
422 | ||
423 | use PVE::ProcFSTools; | |
424 | ||
425 | use base qw(PMG::Config::Base); | |
426 | ||
427 | sub type { | |
428 | return 'mail'; | |
429 | } | |
430 | ||
431 | my $physicalmem = 0; | |
432 | sub physical_memory { | |
433 | ||
434 | return $physicalmem if $physicalmem; | |
435 | ||
436 | my $info = PVE::ProcFSTools::read_meminfo(); | |
437 | my $total = int($info->{memtotal} / (1024*1024)); | |
438 | ||
439 | return $total; | |
440 | } | |
441 | ||
442 | sub get_max_filters { | |
443 | # estimate optimal number of filter servers | |
444 | ||
445 | my $max_servers = 5; | |
446 | my $servermem = 120; | |
447 | my $memory = physical_memory(); | |
448 | my $add_servers = int(($memory - 512)/$servermem); | |
449 | $max_servers += $add_servers if $add_servers > 0; | |
450 | $max_servers = 40 if $max_servers > 40; | |
451 | ||
452 | return $max_servers - 2; | |
453 | } | |
454 | ||
455 | sub get_max_smtpd { | |
456 | # estimate optimal number of smtpd daemons | |
457 | ||
458 | my $max_servers = 25; | |
459 | my $servermem = 20; | |
460 | my $memory = physical_memory(); | |
461 | my $add_servers = int(($memory - 512)/$servermem); | |
462 | $max_servers += $add_servers if $add_servers > 0; | |
463 | $max_servers = 100 if $max_servers > 100; | |
464 | return $max_servers; | |
465 | } | |
466 | ||
467 | sub get_max_policy { | |
468 | # estimate optimal number of proxpolicy servers | |
469 | my $max_servers = 2; | |
470 | my $memory = physical_memory(); | |
471 | $max_servers = 5 if $memory >= 500; | |
472 | return $max_servers; | |
473 | } | |
474 | ||
475 | sub properties { | |
476 | return { | |
477 | int_port => { | |
478 | description => "SMTP port number for outgoing mail (trusted).", | |
479 | type => 'integer', | |
480 | minimum => 1, | |
481 | maximum => 65535, | |
482 | default => 26, | |
483 | }, | |
484 | ext_port => { | |
485 | description => "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.", | |
486 | type => 'integer', | |
487 | minimum => 1, | |
488 | maximum => 65535, | |
489 | default => 25, | |
490 | }, | |
491 | relay => { | |
492 | description => "The default mail delivery transport (incoming mails).", | |
493 | type => 'string', format => 'address', | |
494 | }, | |
495 | relayprotocol => { | |
496 | description => "Transport protocol for relay host.", | |
497 | type => 'string', | |
498 | enum => [qw(smtp lmtp)], | |
499 | default => 'smtp', | |
500 | }, | |
501 | relayport => { | |
502 | description => "SMTP/LMTP port number for relay host.", | |
503 | type => 'integer', | |
504 | minimum => 1, | |
505 | maximum => 65535, | |
506 | default => 25, | |
507 | }, | |
508 | relaynomx => { | |
509 | description => "Disable MX lookups for default relay (SMTP only, ignored for LMTP).", | |
510 | type => 'boolean', | |
511 | default => 0, | |
512 | }, | |
513 | smarthost => { | |
514 | description => "When set, all outgoing mails are deliverd to the specified smarthost.", | |
515 | type => 'string', format => 'address', | |
516 | }, | |
517 | smarthostport => { | |
518 | description => "SMTP port number for smarthost.", | |
519 | type => 'integer', | |
520 | minimum => 1, | |
521 | maximum => 65535, | |
522 | default => 25, | |
523 | }, | |
524 | banner => { | |
525 | description => "ESMTP banner.", | |
526 | type => 'string', | |
527 | maxLength => 1024, | |
528 | default => 'ESMTP Proxmox', | |
529 | }, | |
530 | max_filters => { | |
531 | description => "Maximum number of pmg-smtp-filter processes.", | |
532 | type => 'integer', | |
533 | minimum => 3, | |
534 | maximum => 40, | |
535 | default => get_max_filters(), | |
536 | }, | |
537 | max_policy => { | |
538 | description => "Maximum number of pmgpolicy processes.", | |
539 | type => 'integer', | |
540 | minimum => 2, | |
541 | maximum => 10, | |
542 | default => get_max_policy(), | |
543 | }, | |
544 | max_smtpd_in => { | |
545 | description => "Maximum number of SMTP daemon processes (in).", | |
546 | type => 'integer', | |
547 | minimum => 3, | |
548 | maximum => 100, | |
549 | default => get_max_smtpd(), | |
550 | }, | |
551 | max_smtpd_out => { | |
552 | description => "Maximum number of SMTP daemon processes (out).", | |
553 | type => 'integer', | |
554 | minimum => 3, | |
555 | maximum => 100, | |
556 | default => get_max_smtpd(), | |
557 | }, | |
558 | conn_count_limit => { | |
559 | description => "How many simultaneous connections any client is allowed to make to this service. To disable this feature, specify a limit of 0.", | |
560 | type => 'integer', | |
561 | minimum => 0, | |
562 | default => 50, | |
563 | }, | |
564 | conn_rate_limit => { | |
565 | 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.", | |
566 | type => 'integer', | |
567 | minimum => 0, | |
568 | default => 0, | |
569 | }, | |
570 | message_rate_limit => { | |
571 | 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.", | |
572 | type => 'integer', | |
573 | minimum => 0, | |
574 | default => 0, | |
575 | }, | |
576 | hide_received => { | |
577 | description => "Hide received header in outgoing mails.", | |
578 | type => 'boolean', | |
579 | default => 0, | |
580 | }, | |
581 | maxsize => { | |
582 | description => "Maximum email size. Larger mails are rejected.", | |
583 | type => 'integer', | |
584 | minimum => 1024, | |
585 | default => 1024*1024*10, | |
586 | }, | |
587 | dwarning => { | |
588 | description => "SMTP delay warning time (in hours).", | |
589 | type => 'integer', | |
590 | minimum => 0, | |
591 | default => 4, | |
592 | }, | |
593 | tls => { | |
594 | description => "Enable TLS.", | |
595 | type => 'boolean', | |
596 | default => 0, | |
597 | }, | |
598 | tlslog => { | |
599 | description => "Enable TLS Logging.", | |
600 | type => 'boolean', | |
601 | default => 0, | |
602 | }, | |
603 | tlsheader => { | |
604 | description => "Add TLS received header.", | |
605 | type => 'boolean', | |
606 | default => 0, | |
607 | }, | |
608 | spf => { | |
609 | description => "Use Sender Policy Framework.", | |
610 | type => 'boolean', | |
611 | default => 1, | |
612 | }, | |
613 | greylist => { | |
614 | description => "Use Greylisting for IPv4.", | |
615 | type => 'boolean', | |
616 | default => 1, | |
617 | }, | |
618 | greylistmask4 => { | |
619 | description => "Netmask to apply for greylisting IPv4 hosts", | |
620 | type => 'integer', | |
621 | minimum => 0, | |
622 | maximum => 32, | |
623 | default => 24, | |
624 | }, | |
625 | greylist6 => { | |
626 | description => "Use Greylisting for IPv6.", | |
627 | type => 'boolean', | |
628 | default => 0, | |
629 | }, | |
630 | greylistmask6 => { | |
631 | description => "Netmask to apply for greylisting IPv6 hosts", | |
632 | type => 'integer', | |
633 | minimum => 0, | |
634 | maximum => 128, | |
635 | default => 64, | |
636 | }, | |
637 | helotests => { | |
638 | description => "Use SMTP HELO tests.", | |
639 | type => 'boolean', | |
640 | default => 0, | |
641 | }, | |
642 | rejectunknown => { | |
643 | description => "Reject unknown clients.", | |
644 | type => 'boolean', | |
645 | default => 0, | |
646 | }, | |
647 | rejectunknownsender => { | |
648 | description => "Reject unknown senders.", | |
649 | type => 'boolean', | |
650 | default => 0, | |
651 | }, | |
652 | verifyreceivers => { | |
653 | description => "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.", | |
654 | type => 'string', | |
655 | enum => ['450', '550'], | |
656 | }, | |
657 | dnsbl_sites => { | |
658 | description => "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).", | |
659 | type => 'string', format => 'dnsbl-entry-list', | |
660 | }, | |
661 | dnsbl_threshold => { | |
662 | description => "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).", | |
663 | type => 'integer', | |
664 | minimum => 0, | |
665 | default => 1 | |
666 | }, | |
667 | before_queue_filtering => { | |
668 | description => "Enable before queue filtering by pmg-smtp-filter", | |
669 | type => 'boolean', | |
670 | default => 0 | |
671 | }, | |
672 | ndr_on_block => { | |
673 | description => "Send out NDR when mail gets blocked", | |
674 | type => 'boolean', | |
675 | default => 0 | |
676 | }, | |
677 | }; | |
678 | } | |
679 | ||
680 | sub options { | |
681 | return { | |
682 | int_port => { optional => 1 }, | |
683 | ext_port => { optional => 1 }, | |
684 | smarthost => { optional => 1 }, | |
685 | smarthostport => { optional => 1 }, | |
686 | relay => { optional => 1 }, | |
687 | relayprotocol => { optional => 1 }, | |
688 | relayport => { optional => 1 }, | |
689 | relaynomx => { optional => 1 }, | |
690 | dwarning => { optional => 1 }, | |
691 | max_smtpd_in => { optional => 1 }, | |
692 | max_smtpd_out => { optional => 1 }, | |
693 | greylist => { optional => 1 }, | |
694 | greylistmask4 => { optional => 1 }, | |
695 | greylist6 => { optional => 1 }, | |
696 | greylistmask6 => { optional => 1 }, | |
697 | helotests => { optional => 1 }, | |
698 | tls => { optional => 1 }, | |
699 | tlslog => { optional => 1 }, | |
700 | tlsheader => { optional => 1 }, | |
701 | spf => { optional => 1 }, | |
702 | maxsize => { optional => 1 }, | |
703 | banner => { optional => 1 }, | |
704 | max_filters => { optional => 1 }, | |
705 | max_policy => { optional => 1 }, | |
706 | hide_received => { optional => 1 }, | |
707 | rejectunknown => { optional => 1 }, | |
708 | rejectunknownsender => { optional => 1 }, | |
709 | conn_count_limit => { optional => 1 }, | |
710 | conn_rate_limit => { optional => 1 }, | |
711 | message_rate_limit => { optional => 1 }, | |
712 | verifyreceivers => { optional => 1 }, | |
713 | dnsbl_sites => { optional => 1 }, | |
714 | dnsbl_threshold => { optional => 1 }, | |
715 | before_queue_filtering => { optional => 1 }, | |
716 | ndr_on_block => { optional => 1 }, | |
717 | }; | |
718 | } | |
719 | ||
720 | package PMG::Config; | |
721 | ||
722 | use strict; | |
723 | use warnings; | |
724 | use IO::File; | |
725 | use Data::Dumper; | |
726 | use Template; | |
727 | ||
728 | use PVE::SafeSyslog; | |
729 | use PVE::Tools qw($IPV4RE $IPV6RE); | |
730 | use PVE::INotify; | |
731 | use PVE::JSONSchema; | |
732 | ||
733 | use PMG::Cluster; | |
734 | use PMG::Utils; | |
735 | ||
736 | PMG::Config::Admin->register(); | |
737 | PMG::Config::Mail->register(); | |
738 | PMG::Config::SpamQuarantine->register(); | |
739 | PMG::Config::VirusQuarantine->register(); | |
740 | PMG::Config::Spam->register(); | |
741 | PMG::Config::ClamAV->register(); | |
742 | ||
743 | # initialize all plugins | |
744 | PMG::Config::Base->init(); | |
745 | ||
746 | PVE::JSONSchema::register_format( | |
747 | 'transport-domain', \&pmg_verify_transport_domain); | |
748 | ||
749 | sub pmg_verify_transport_domain { | |
750 | my ($name, $noerr) = @_; | |
751 | ||
752 | # like dns-name, but can contain leading dot | |
753 | my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)"; | |
754 | ||
755 | if ($name !~ /^\.?(${namere}\.)*${namere}$/) { | |
756 | return undef if $noerr; | |
757 | die "value does not look like a valid transport domain\n"; | |
758 | } | |
759 | return $name; | |
760 | } | |
761 | ||
762 | PVE::JSONSchema::register_format( | |
763 | 'transport-domain-or-email', \&pmg_verify_transport_domain_or_email); | |
764 | ||
765 | sub pmg_verify_transport_domain_or_email { | |
766 | my ($name, $noerr) = @_; | |
767 | ||
768 | my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)"; | |
769 | ||
770 | # email address | |
771 | if ($name =~ m/^(?:[^\s\/\@]+\@)(${namere}\.)*${namere}$/) { | |
772 | return $name; | |
773 | } | |
774 | ||
775 | # like dns-name, but can contain leading dot | |
776 | if ($name !~ /^\.?(${namere}\.)*${namere}$/) { | |
777 | return undef if $noerr; | |
778 | die "value does not look like a valid transport domain or email address\n"; | |
779 | } | |
780 | return $name; | |
781 | } | |
782 | ||
783 | PVE::JSONSchema::register_format( | |
784 | 'dnsbl-entry', \&pmg_verify_dnsbl_entry); | |
785 | ||
786 | sub pmg_verify_dnsbl_entry { | |
787 | my ($name, $noerr) = @_; | |
788 | ||
789 | # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>' | |
790 | # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites | |
791 | # we don't implement the ';' separated numbers in pattern, because this | |
792 | # breaks at PVE::JSONSchema::split_list | |
793 | my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)"; | |
794 | ||
795 | my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/; | |
796 | my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/; | |
797 | if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) { | |
798 | return undef if $noerr; | |
799 | die "value '$name' does not look like a valid dnsbl entry\n"; | |
800 | } | |
801 | return $name; | |
802 | } | |
803 | ||
804 | sub new { | |
805 | my ($type) = @_; | |
806 | ||
807 | my $class = ref($type) || $type; | |
808 | ||
809 | my $cfg = PVE::INotify::read_file("pmg.conf"); | |
810 | ||
811 | return bless $cfg, $class; | |
812 | } | |
813 | ||
814 | sub write { | |
815 | my ($self) = @_; | |
816 | ||
817 | PVE::INotify::write_file("pmg.conf", $self); | |
818 | } | |
819 | ||
820 | my $lockfile = "/var/lock/pmgconfig.lck"; | |
821 | ||
822 | sub lock_config { | |
823 | my ($code, $errmsg) = @_; | |
824 | ||
825 | my $p = PVE::Tools::lock_file($lockfile, undef, $code); | |
826 | if (my $err = $@) { | |
827 | $errmsg ? die "$errmsg: $err" : die $err; | |
828 | } | |
829 | } | |
830 | ||
831 | # set section values | |
832 | sub set { | |
833 | my ($self, $section, $key, $value) = @_; | |
834 | ||
835 | my $pdata = PMG::Config::Base->private(); | |
836 | ||
837 | my $plugin = $pdata->{plugins}->{$section}; | |
838 | die "no such section '$section'" if !$plugin; | |
839 | ||
840 | if (defined($value)) { | |
841 | my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0); | |
842 | $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section}); | |
843 | $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp); | |
844 | } else { | |
845 | if (defined($self->{ids}->{$section})) { | |
846 | delete $self->{ids}->{$section}->{$key}; | |
847 | } | |
848 | } | |
849 | ||
850 | return undef; | |
851 | } | |
852 | ||
853 | # get section value or default | |
854 | sub get { | |
855 | my ($self, $section, $key, $nodefault) = @_; | |
856 | ||
857 | my $pdata = PMG::Config::Base->private(); | |
858 | my $pdesc = $pdata->{propertyList}->{$key}; | |
859 | die "no such property '$section/$key'\n" | |
860 | if !(defined($pdesc) && defined($pdata->{options}->{$section}) && | |
861 | defined($pdata->{options}->{$section}->{$key})); | |
862 | ||
863 | if (defined($self->{ids}->{$section}) && | |
864 | defined(my $value = $self->{ids}->{$section}->{$key})) { | |
865 | return $value; | |
866 | } | |
867 | ||
868 | return undef if $nodefault; | |
869 | ||
870 | return $pdesc->{default}; | |
871 | } | |
872 | ||
873 | # get a whole section with default value | |
874 | sub get_section { | |
875 | my ($self, $section) = @_; | |
876 | ||
877 | my $pdata = PMG::Config::Base->private(); | |
878 | return undef if !defined($pdata->{options}->{$section}); | |
879 | ||
880 | my $res = {}; | |
881 | ||
882 | foreach my $key (keys %{$pdata->{options}->{$section}}) { | |
883 | ||
884 | my $pdesc = $pdata->{propertyList}->{$key}; | |
885 | ||
886 | if (defined($self->{ids}->{$section}) && | |
887 | defined(my $value = $self->{ids}->{$section}->{$key})) { | |
888 | $res->{$key} = $value; | |
889 | next; | |
890 | } | |
891 | $res->{$key} = $pdesc->{default}; | |
892 | } | |
893 | ||
894 | return $res; | |
895 | } | |
896 | ||
897 | # get a whole config with default values | |
898 | sub get_config { | |
899 | my ($self) = @_; | |
900 | ||
901 | my $pdata = PMG::Config::Base->private(); | |
902 | ||
903 | my $res = {}; | |
904 | ||
905 | foreach my $type (keys %{$pdata->{plugins}}) { | |
906 | my $plugin = $pdata->{plugins}->{$type}; | |
907 | $res->{$type} = $self->get_section($type); | |
908 | } | |
909 | ||
910 | return $res; | |
911 | } | |
912 | ||
913 | sub read_pmg_conf { | |
914 | my ($filename, $fh) = @_; | |
915 | ||
916 | local $/ = undef; # slurp mode | |
917 | ||
918 | my $raw = <$fh> if defined($fh); | |
919 | ||
920 | return PMG::Config::Base->parse_config($filename, $raw); | |
921 | } | |
922 | ||
923 | sub write_pmg_conf { | |
924 | my ($filename, $fh, $cfg) = @_; | |
925 | ||
926 | my $raw = PMG::Config::Base->write_config($filename, $cfg); | |
927 | ||
928 | PVE::Tools::safe_print($filename, $fh, $raw); | |
929 | } | |
930 | ||
931 | PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf", | |
932 | \&read_pmg_conf, | |
933 | \&write_pmg_conf, | |
934 | undef, always_call_parser => 1); | |
935 | ||
936 | # parsers/writers for other files | |
937 | ||
938 | my $domainsfilename = "/etc/pmg/domains"; | |
939 | ||
940 | sub postmap_pmg_domains { | |
941 | PMG::Utils::run_postmap($domainsfilename); | |
942 | } | |
943 | ||
944 | sub read_pmg_domains { | |
945 | my ($filename, $fh) = @_; | |
946 | ||
947 | my $domains = {}; | |
948 | ||
949 | my $comment = ''; | |
950 | if (defined($fh)) { | |
951 | while (defined(my $line = <$fh>)) { | |
952 | chomp $line; | |
953 | next if $line =~ m/^\s*$/; | |
954 | if ($line =~ m/^#(.*)\s*$/) { | |
955 | $comment = $1; | |
956 | next; | |
957 | } | |
958 | if ($line =~ m/^(\S+)\s.*$/) { | |
959 | my $domain = $1; | |
960 | $domains->{$domain} = { | |
961 | domain => $domain, comment => $comment }; | |
962 | $comment = ''; | |
963 | } else { | |
964 | warn "parse error in '$filename': $line\n"; | |
965 | $comment = ''; | |
966 | } | |
967 | } | |
968 | } | |
969 | ||
970 | return $domains; | |
971 | } | |
972 | ||
973 | sub write_pmg_domains { | |
974 | my ($filename, $fh, $domains) = @_; | |
975 | ||
976 | foreach my $domain (sort keys %$domains) { | |
977 | my $comment = $domains->{$domain}->{comment}; | |
978 | PVE::Tools::safe_print($filename, $fh, "#$comment\n") | |
979 | if defined($comment) && $comment !~ m/^\s*$/; | |
980 | ||
981 | PVE::Tools::safe_print($filename, $fh, "$domain 1\n"); | |
982 | } | |
983 | } | |
984 | ||
985 | PVE::INotify::register_file('domains', $domainsfilename, | |
986 | \&read_pmg_domains, | |
987 | \&write_pmg_domains, | |
988 | undef, always_call_parser => 1); | |
989 | ||
990 | my $dkimdomainsfile = '/etc/pmg/dkim/domains'; | |
991 | ||
992 | PVE::INotify::register_file('dkimdomains', $dkimdomainsfile, | |
993 | \&read_pmg_domains, | |
994 | \&write_pmg_domains, | |
995 | undef, always_call_parser => 1); | |
996 | ||
997 | my $mynetworks_filename = "/etc/pmg/mynetworks"; | |
998 | ||
999 | sub read_pmg_mynetworks { | |
1000 | my ($filename, $fh) = @_; | |
1001 | ||
1002 | my $mynetworks = {}; | |
1003 | ||
1004 | my $comment = ''; | |
1005 | if (defined($fh)) { | |
1006 | while (defined(my $line = <$fh>)) { | |
1007 | chomp $line; | |
1008 | next if $line =~ m/^\s*$/; | |
1009 | if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) { | |
1010 | my ($network, $prefix_size, $comment) = ($1, $2, $3); | |
1011 | my $cidr = "$network/${prefix_size}"; | |
1012 | $mynetworks->{$cidr} = { | |
1013 | cidr => $cidr, | |
1014 | network_address => $network, | |
1015 | prefix_size => $prefix_size, | |
1016 | comment => $comment // '', | |
1017 | }; | |
1018 | } else { | |
1019 | warn "parse error in '$filename': $line\n"; | |
1020 | } | |
1021 | } | |
1022 | } | |
1023 | ||
1024 | return $mynetworks; | |
1025 | } | |
1026 | ||
1027 | sub write_pmg_mynetworks { | |
1028 | my ($filename, $fh, $mynetworks) = @_; | |
1029 | ||
1030 | foreach my $cidr (sort keys %$mynetworks) { | |
1031 | my $data = $mynetworks->{$cidr}; | |
1032 | my $comment = $data->{comment} // '*'; | |
1033 | PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n"); | |
1034 | } | |
1035 | } | |
1036 | ||
1037 | PVE::INotify::register_file('mynetworks', $mynetworks_filename, | |
1038 | \&read_pmg_mynetworks, | |
1039 | \&write_pmg_mynetworks, | |
1040 | undef, always_call_parser => 1); | |
1041 | ||
1042 | PVE::JSONSchema::register_format( | |
1043 | 'tls-policy', \&pmg_verify_tls_policy); | |
1044 | ||
1045 | # TODO: extend to parse attributes of the policy | |
1046 | my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/; | |
1047 | sub pmg_verify_tls_policy { | |
1048 | my ($policy, $noerr) = @_; | |
1049 | ||
1050 | if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) { | |
1051 | return undef if $noerr; | |
1052 | die "value '$policy' does not look like a valid tls policy\n"; | |
1053 | } | |
1054 | return $policy; | |
1055 | } | |
1056 | ||
1057 | PVE::JSONSchema::register_format( | |
1058 | 'tls-policy-strict', \&pmg_verify_tls_policy_strict); | |
1059 | ||
1060 | sub pmg_verify_tls_policy_strict { | |
1061 | my ($policy, $noerr) = @_; | |
1062 | ||
1063 | if ($policy !~ /^$VALID_TLS_POLICY_RE$/) { | |
1064 | return undef if $noerr; | |
1065 | die "value '$policy' does not look like a valid tls policy\n"; | |
1066 | } | |
1067 | return $policy; | |
1068 | } | |
1069 | ||
1070 | PVE::JSONSchema::register_format( | |
1071 | 'transport-domain-or-nexthop', \&pmg_verify_transport_domain_or_nexthop); | |
1072 | ||
1073 | sub pmg_verify_transport_domain_or_nexthop { | |
1074 | my ($name, $noerr) = @_; | |
1075 | ||
1076 | if (pmg_verify_transport_domain($name, 1)) { | |
1077 | return $name; | |
1078 | } elsif ($name =~ m/^(\S+)(?::\d+)?$/) { | |
1079 | my $nexthop = $1; | |
1080 | if ($nexthop =~ m/^\[(.*)\]$/) { | |
1081 | $nexthop = $1; | |
1082 | } | |
1083 | return $name if pmg_verify_transport_address($nexthop, 1); | |
1084 | } else { | |
1085 | return undef if $noerr; | |
1086 | die "value does not look like a valid domain or next-hop\n"; | |
1087 | } | |
1088 | } | |
1089 | ||
1090 | sub read_tls_policy { | |
1091 | my ($filename, $fh) = @_; | |
1092 | ||
1093 | return {} if !defined($fh); | |
1094 | ||
1095 | my $tls_policy = {}; | |
1096 | ||
1097 | while (defined(my $line = <$fh>)) { | |
1098 | chomp $line; | |
1099 | next if $line =~ m/^\s*$/; | |
1100 | next if $line =~ m/^#(.*)\s*$/; | |
1101 | ||
1102 | my $parse_error = sub { | |
1103 | my ($err) = @_; | |
1104 | die "parse error in '$filename': $line - $err"; | |
1105 | }; | |
1106 | ||
1107 | if ($line =~ m/^(\S+)\s+(.+)\s*$/) { | |
1108 | my ($destination, $policy) = ($1, $2); | |
1109 | ||
1110 | eval { | |
1111 | pmg_verify_transport_domain_or_nexthop($destination); | |
1112 | pmg_verify_tls_policy($policy); | |
1113 | }; | |
1114 | if (my $err = $@) { | |
1115 | $parse_error->($err); | |
1116 | next; | |
1117 | } | |
1118 | ||
1119 | $tls_policy->{$destination} = { | |
1120 | destination => $destination, | |
1121 | policy => $policy, | |
1122 | }; | |
1123 | } else { | |
1124 | $parse_error->('wrong format'); | |
1125 | } | |
1126 | } | |
1127 | ||
1128 | return $tls_policy; | |
1129 | } | |
1130 | ||
1131 | sub write_tls_policy { | |
1132 | my ($filename, $fh, $tls_policy) = @_; | |
1133 | ||
1134 | return if !$tls_policy; | |
1135 | ||
1136 | foreach my $destination (sort keys %$tls_policy) { | |
1137 | my $entry = $tls_policy->{$destination}; | |
1138 | PVE::Tools::safe_print( | |
1139 | $filename, $fh, "$entry->{destination} $entry->{policy}\n"); | |
1140 | } | |
1141 | } | |
1142 | ||
1143 | my $tls_policy_map_filename = "/etc/pmg/tls_policy"; | |
1144 | PVE::INotify::register_file('tls_policy', $tls_policy_map_filename, | |
1145 | \&read_tls_policy, | |
1146 | \&write_tls_policy, | |
1147 | undef, always_call_parser => 1); | |
1148 | ||
1149 | sub postmap_tls_policy { | |
1150 | PMG::Utils::run_postmap($tls_policy_map_filename); | |
1151 | } | |
1152 | ||
1153 | my $transport_map_filename = "/etc/pmg/transport"; | |
1154 | ||
1155 | sub postmap_pmg_transport { | |
1156 | PMG::Utils::run_postmap($transport_map_filename); | |
1157 | } | |
1158 | ||
1159 | PVE::JSONSchema::register_format( | |
1160 | 'transport-address', \&pmg_verify_transport_address); | |
1161 | ||
1162 | sub pmg_verify_transport_address { | |
1163 | my ($name, $noerr) = @_; | |
1164 | ||
1165 | if ($name =~ m/^ipv6:($IPV6RE)$/i) { | |
1166 | return $name; | |
1167 | } elsif (PVE::JSONSchema::pve_verify_address($name, 1)) { | |
1168 | return $name; | |
1169 | } else { | |
1170 | return undef if $noerr; | |
1171 | die "value does not look like a valid address\n"; | |
1172 | } | |
1173 | } | |
1174 | ||
1175 | sub read_transport_map { | |
1176 | my ($filename, $fh) = @_; | |
1177 | ||
1178 | return [] if !defined($fh); | |
1179 | ||
1180 | my $res = {}; | |
1181 | ||
1182 | my $comment = ''; | |
1183 | ||
1184 | while (defined(my $line = <$fh>)) { | |
1185 | chomp $line; | |
1186 | next if $line =~ m/^\s*$/; | |
1187 | if ($line =~ m/^#(.*)\s*$/) { | |
1188 | $comment = $1; | |
1189 | next; | |
1190 | } | |
1191 | ||
1192 | my $parse_error = sub { | |
1193 | my ($err) = @_; | |
1194 | warn "parse error in '$filename': $line - $err"; | |
1195 | $comment = ''; | |
1196 | }; | |
1197 | ||
1198 | if ($line =~ m/^(\S+)\s+(?:(lmtp):inet|(smtp)):(\S+):(\d+)\s*$/) { | |
1199 | my ($domain, $protocol, $host, $port) = ($1, ($2 or $3), $4, $5); | |
1200 | ||
1201 | eval { pmg_verify_transport_domain_or_email($domain); }; | |
1202 | if (my $err = $@) { | |
1203 | $parse_error->($err); | |
1204 | next; | |
1205 | } | |
1206 | my $use_mx = 1; | |
1207 | if ($host =~ m/^\[(.*)\]$/) { | |
1208 | $host = $1; | |
1209 | $use_mx = 0; | |
1210 | } | |
1211 | $use_mx = 0 if ($protocol eq "lmtp"); | |
1212 | ||
1213 | eval { pmg_verify_transport_address($host); }; | |
1214 | if (my $err = $@) { | |
1215 | $parse_error->($err); | |
1216 | next; | |
1217 | } | |
1218 | ||
1219 | my $data = { | |
1220 | domain => $domain, | |
1221 | protocol => $protocol, | |
1222 | host => $host, | |
1223 | port => $port, | |
1224 | use_mx => $use_mx, | |
1225 | comment => $comment, | |
1226 | }; | |
1227 | $res->{$domain} = $data; | |
1228 | $comment = ''; | |
1229 | } else { | |
1230 | $parse_error->('wrong format'); | |
1231 | } | |
1232 | } | |
1233 | ||
1234 | return $res; | |
1235 | } | |
1236 | ||
1237 | sub write_transport_map { | |
1238 | my ($filename, $fh, $tmap) = @_; | |
1239 | ||
1240 | return if !$tmap; | |
1241 | ||
1242 | foreach my $domain (sort keys %$tmap) { | |
1243 | my $data = $tmap->{$domain}; | |
1244 | ||
1245 | my $comment = $data->{comment}; | |
1246 | PVE::Tools::safe_print($filename, $fh, "#$comment\n") | |
1247 | if defined($comment) && $comment !~ m/^\s*$/; | |
1248 | ||
1249 | my $bracket_host = !$data->{use_mx}; | |
1250 | ||
1251 | if ($data->{protocol} eq 'lmtp') { | |
1252 | $bracket_host = 0; | |
1253 | $data->{protocol} .= ":inet"; | |
1254 | } | |
1255 | $bracket_host = 1 if $data->{host} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i; | |
1256 | my $host = $bracket_host ? "[$data->{host}]" : $data->{host}; | |
1257 | ||
1258 | PVE::Tools::safe_print($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n"); | |
1259 | } | |
1260 | } | |
1261 | ||
1262 | PVE::INotify::register_file('transport', $transport_map_filename, | |
1263 | \&read_transport_map, | |
1264 | \&write_transport_map, | |
1265 | undef, always_call_parser => 1); | |
1266 | ||
1267 | # config file generation using templates | |
1268 | ||
1269 | sub get_host_dns_info { | |
1270 | my ($self) = @_; | |
1271 | ||
1272 | my $dnsinfo = {}; | |
1273 | my $nodename = PVE::INotify::nodename(); | |
1274 | ||
1275 | $dnsinfo->{hostname} = $nodename; | |
1276 | my $resolv = PVE::INotify::read_file('resolvconf'); | |
1277 | ||
1278 | my $domain = $resolv->{search} // 'localdomain'; | |
1279 | $dnsinfo->{domain} = $domain; | |
1280 | ||
1281 | $dnsinfo->{fqdn} = "$nodename.$domain"; | |
1282 | ||
1283 | return $dnsinfo; | |
1284 | } | |
1285 | ||
1286 | sub get_template_vars { | |
1287 | my ($self) = @_; | |
1288 | ||
1289 | my $vars = { pmg => $self->get_config() }; | |
1290 | ||
1291 | my $dnsinfo = get_host_dns_info(); | |
1292 | $vars->{dns} = $dnsinfo; | |
1293 | my $int_ip = PMG::Cluster::remote_node_ip($dnsinfo->{hostname}); | |
1294 | $vars->{ipconfig}->{int_ip} = $int_ip; | |
1295 | ||
1296 | my $transportnets = []; | |
1297 | ||
1298 | if (my $tmap = PVE::INotify::read_file('transport')) { | |
1299 | foreach my $domain (sort keys %$tmap) { | |
1300 | my $data = $tmap->{$domain}; | |
1301 | my $host = $data->{host}; | |
1302 | if ($host =~ m/^$IPV4RE$/) { | |
1303 | push @$transportnets, "$host/32"; | |
1304 | } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) { | |
1305 | push @$transportnets, "[$1]/128"; | |
1306 | } | |
1307 | } | |
1308 | } | |
1309 | ||
1310 | $vars->{postfix}->{transportnets} = join(' ', @$transportnets); | |
1311 | ||
1312 | my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ]; | |
1313 | ||
1314 | if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) { | |
1315 | if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) { | |
1316 | push @$mynetworks, "[$1]/$2"; | |
1317 | } else { | |
1318 | push @$mynetworks, $int_net_cidr; | |
1319 | } | |
1320 | } else { | |
1321 | if ($int_ip =~ m/^$IPV6RE$/) { | |
1322 | push @$mynetworks, "[$int_ip]/128"; | |
1323 | } else { | |
1324 | push @$mynetworks, "$int_ip/32"; | |
1325 | } | |
1326 | } | |
1327 | ||
1328 | my $netlist = PVE::INotify::read_file('mynetworks'); | |
1329 | foreach my $cidr (sort keys %$netlist) { | |
1330 | if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) { | |
1331 | push @$mynetworks, "[$1]/$2"; | |
1332 | } else { | |
1333 | push @$mynetworks, $cidr; | |
1334 | } | |
1335 | } | |
1336 | ||
1337 | push @$mynetworks, @$transportnets; | |
1338 | ||
1339 | # add default relay to mynetworks | |
1340 | if (my $relay = $self->get('mail', 'relay')) { | |
1341 | if ($relay =~ m/^$IPV4RE$/) { | |
1342 | push @$mynetworks, "$relay/32"; | |
1343 | } elsif ($relay =~ m/^$IPV6RE$/) { | |
1344 | push @$mynetworks, "[$relay]/128"; | |
1345 | } else { | |
1346 | # DNS name - do nothing ? | |
1347 | } | |
1348 | } | |
1349 | ||
1350 | $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks); | |
1351 | ||
1352 | # normalize dnsbl_sites | |
1353 | my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites}); | |
1354 | if (scalar(@dnsbl_sites)) { | |
1355 | $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites); | |
1356 | } | |
1357 | ||
1358 | $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold'); | |
1359 | ||
1360 | my $usepolicy = 0; | |
1361 | $usepolicy = 1 if $self->get('mail', 'greylist') || | |
1362 | $self->get('mail', 'greylist6') || $self->get('mail', 'spf'); | |
1363 | $vars->{postfix}->{usepolicy} = $usepolicy; | |
1364 | ||
1365 | if ($int_ip =~ m/^$IPV6RE$/) { | |
1366 | $vars->{postfix}->{int_ip} = "[$int_ip]"; | |
1367 | } else { | |
1368 | $vars->{postfix}->{int_ip} = $int_ip; | |
1369 | } | |
1370 | ||
1371 | my $wlbr = $dnsinfo->{fqdn}; | |
1372 | foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) { | |
1373 | $wlbr .= " $r" | |
1374 | } | |
1375 | $vars->{composed}->{wl_bounce_relays} = $wlbr; | |
1376 | ||
1377 | if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) { | |
1378 | eval { | |
1379 | my $uri = URI->new($proxy); | |
1380 | my $host = $uri->host; | |
1381 | my $port = $uri->port // 8080; | |
1382 | if ($host) { | |
1383 | my $data = { host => $host, port => $port }; | |
1384 | if (my $ui = $uri->userinfo) { | |
1385 | my ($username, $pw) = split(/:/, $ui, 2); | |
1386 | $data->{username} = $username; | |
1387 | $data->{password} = $pw if defined($pw); | |
1388 | } | |
1389 | $vars->{proxy} = $data; | |
1390 | } | |
1391 | }; | |
1392 | warn "parse http_proxy failed - $@" if $@; | |
1393 | } | |
1394 | $vars->{postgres}->{version} = PMG::Utils::get_pg_server_version(); | |
1395 | ||
1396 | return $vars; | |
1397 | } | |
1398 | ||
1399 | # use one global TT cache | |
1400 | our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ]; | |
1401 | ||
1402 | my $template_toolkit; | |
1403 | ||
1404 | sub get_template_toolkit { | |
1405 | ||
1406 | return $template_toolkit if $template_toolkit; | |
1407 | ||
1408 | $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path }); | |
1409 | ||
1410 | return $template_toolkit; | |
1411 | } | |
1412 | ||
1413 | # rewrite file from template | |
1414 | # return true if file has changed | |
1415 | sub rewrite_config_file { | |
1416 | my ($self, $tmplname, $dstfn) = @_; | |
1417 | ||
1418 | my $demo = $self->get('admin', 'demo'); | |
1419 | ||
1420 | if ($demo) { | |
1421 | my $demosrc = "$tmplname.demo"; | |
1422 | $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc"; | |
1423 | } | |
1424 | ||
1425 | my ($perm, $uid, $gid); | |
1426 | ||
1427 | if ($dstfn eq '/etc/clamav/freshclam.conf') { | |
1428 | # needed if file contains a HTTPProxyPasswort | |
1429 | ||
1430 | $uid = getpwnam('clamav'); | |
1431 | $gid = getgrnam('adm'); | |
1432 | $perm = 0600; | |
1433 | } | |
1434 | ||
1435 | my $tt = get_template_toolkit(); | |
1436 | ||
1437 | my $vars = $self->get_template_vars(); | |
1438 | ||
1439 | my $output = ''; | |
1440 | ||
1441 | $tt->process($tmplname, $vars, \$output) || | |
1442 | die $tt->error() . "\n"; | |
1443 | ||
1444 | my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn; | |
1445 | ||
1446 | return 0 if defined($old) && ($old eq $output); # no change | |
1447 | ||
1448 | PVE::Tools::file_set_contents($dstfn, $output, $perm); | |
1449 | ||
1450 | if (defined($uid) && defined($gid)) { | |
1451 | chown($uid, $gid, $dstfn); | |
1452 | } | |
1453 | ||
1454 | return 1; | |
1455 | } | |
1456 | ||
1457 | # rewrite spam configuration | |
1458 | sub rewrite_config_spam { | |
1459 | my ($self) = @_; | |
1460 | ||
1461 | my $use_awl = $self->get('spam', 'use_awl'); | |
1462 | my $use_bayes = $self->get('spam', 'use_bayes'); | |
1463 | my $use_razor = $self->get('spam', 'use_razor'); | |
1464 | ||
1465 | my $changes = 0; | |
1466 | ||
1467 | # delete AW and bayes databases if those features are disabled | |
1468 | if (!$use_awl) { | |
1469 | $changes = 1 if unlink '/root/.spamassassin/auto-whitelist'; | |
1470 | } | |
1471 | ||
1472 | if (!$use_bayes) { | |
1473 | $changes = 1 if unlink '/root/.spamassassin/bayes_journal'; | |
1474 | $changes = 1 if unlink '/root/.spamassassin/bayes_seen'; | |
1475 | $changes = 1 if unlink '/root/.spamassassin/bayes_toks'; | |
1476 | } | |
1477 | ||
1478 | # make sure we have the custom SA files (else cluster sync fails) | |
1479 | IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644); | |
1480 | IO::File->new('/etc/mail/spamassassin/pmg-scores.cf', 'a', 0644); | |
1481 | ||
1482 | $changes = 1 if $self->rewrite_config_file( | |
1483 | 'local.cf.in', '/etc/mail/spamassassin/local.cf'); | |
1484 | ||
1485 | $changes = 1 if $self->rewrite_config_file( | |
1486 | 'init.pre.in', '/etc/mail/spamassassin/init.pre'); | |
1487 | ||
1488 | $changes = 1 if $self->rewrite_config_file( | |
1489 | 'v310.pre.in', '/etc/mail/spamassassin/v310.pre'); | |
1490 | ||
1491 | $changes = 1 if $self->rewrite_config_file( | |
1492 | 'v320.pre.in', '/etc/mail/spamassassin/v320.pre'); | |
1493 | ||
1494 | if ($use_razor) { | |
1495 | mkdir "/root/.razor"; | |
1496 | ||
1497 | $changes = 1 if $self->rewrite_config_file( | |
1498 | 'razor-agent.conf.in', '/root/.razor/razor-agent.conf'); | |
1499 | ||
1500 | if (! -e '/root/.razor/identity') { | |
1501 | eval { | |
1502 | my $timeout = 30; | |
1503 | PVE::Tools::run_command(['razor-admin', '-discover'], timeout => $timeout); | |
1504 | PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout); | |
1505 | }; | |
1506 | my $err = $@; | |
1507 | syslog('info', "registering razor failed: $err") if $err; | |
1508 | } | |
1509 | } | |
1510 | ||
1511 | return $changes; | |
1512 | } | |
1513 | ||
1514 | # rewrite ClamAV configuration | |
1515 | sub rewrite_config_clam { | |
1516 | my ($self) = @_; | |
1517 | ||
1518 | return $self->rewrite_config_file( | |
1519 | 'clamd.conf.in', '/etc/clamav/clamd.conf'); | |
1520 | } | |
1521 | ||
1522 | sub rewrite_config_freshclam { | |
1523 | my ($self) = @_; | |
1524 | ||
1525 | return $self->rewrite_config_file( | |
1526 | 'freshclam.conf.in', '/etc/clamav/freshclam.conf'); | |
1527 | } | |
1528 | ||
1529 | sub rewrite_config_postgres { | |
1530 | my ($self) = @_; | |
1531 | ||
1532 | my $pg_maj_version = PMG::Utils::get_pg_server_version(); | |
1533 | my $pgconfdir = "/etc/postgresql/$pg_maj_version/main"; | |
1534 | ||
1535 | my $changes = 0; | |
1536 | ||
1537 | $changes = 1 if $self->rewrite_config_file( | |
1538 | 'pg_hba.conf.in', "$pgconfdir/pg_hba.conf"); | |
1539 | ||
1540 | $changes = 1 if $self->rewrite_config_file( | |
1541 | 'postgresql.conf.in', "$pgconfdir/postgresql.conf"); | |
1542 | ||
1543 | return $changes; | |
1544 | } | |
1545 | ||
1546 | # rewrite /root/.forward | |
1547 | sub rewrite_dot_forward { | |
1548 | my ($self) = @_; | |
1549 | ||
1550 | my $dstfn = '/root/.forward'; | |
1551 | ||
1552 | my $email = $self->get('admin', 'email'); | |
1553 | ||
1554 | my $output = ''; | |
1555 | if ($email && $email =~ m/\s*(\S+)\s*/) { | |
1556 | $output = "$1\n"; | |
1557 | } else { | |
1558 | # empty .forward does not forward mails (see man local) | |
1559 | } | |
1560 | ||
1561 | my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn; | |
1562 | ||
1563 | return 0 if defined($old) && ($old eq $output); # no change | |
1564 | ||
1565 | PVE::Tools::file_set_contents($dstfn, $output); | |
1566 | ||
1567 | return 1; | |
1568 | } | |
1569 | ||
1570 | my $write_smtp_whitelist = sub { | |
1571 | my ($filename, $data, $action) = @_; | |
1572 | ||
1573 | $action = 'OK' if !$action; | |
1574 | ||
1575 | my $old = PVE::Tools::file_get_contents($filename, 1024*1024) | |
1576 | if -f $filename; | |
1577 | ||
1578 | my $new = ''; | |
1579 | foreach my $k (sort keys %$data) { | |
1580 | $new .= "$k $action\n"; | |
1581 | } | |
1582 | ||
1583 | return 0 if defined($old) && ($old eq $new); # no change | |
1584 | ||
1585 | PVE::Tools::file_set_contents($filename, $new); | |
1586 | ||
1587 | PMG::Utils::run_postmap($filename); | |
1588 | ||
1589 | return 1; | |
1590 | }; | |
1591 | ||
1592 | sub rewrite_postfix_whitelist { | |
1593 | my ($rulecache) = @_; | |
1594 | ||
1595 | # see man page for regexp_table for postfix regex table format | |
1596 | ||
1597 | # we use a hash to avoid duplicate entries in regex tables | |
1598 | my $tolist = {}; | |
1599 | my $fromlist = {}; | |
1600 | my $clientlist = {}; | |
1601 | ||
1602 | foreach my $obj (@{$rulecache->{"greylist:receiver"}}) { | |
1603 | my $oclass = ref($obj); | |
1604 | if ($oclass eq 'PMG::RuleDB::Receiver') { | |
1605 | my $addr = PMG::Utils::quote_regex($obj->{address}); | |
1606 | $tolist->{"/^$addr\$/"} = 1; | |
1607 | } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') { | |
1608 | my $addr = PMG::Utils::quote_regex($obj->{address}); | |
1609 | $tolist->{"/^.+\@$addr\$/"} = 1; | |
1610 | } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') { | |
1611 | my $addr = $obj->{address}; | |
1612 | $addr =~ s|/|\\/|g; | |
1613 | $tolist->{"/^$addr\$/"} = 1; | |
1614 | } | |
1615 | } | |
1616 | ||
1617 | foreach my $obj (@{$rulecache->{"greylist:sender"}}) { | |
1618 | my $oclass = ref($obj); | |
1619 | my $addr = PMG::Utils::quote_regex($obj->{address}); | |
1620 | if ($oclass eq 'PMG::RuleDB::EMail') { | |
1621 | my $addr = PMG::Utils::quote_regex($obj->{address}); | |
1622 | $fromlist->{"/^$addr\$/"} = 1; | |
1623 | } elsif ($oclass eq 'PMG::RuleDB::Domain') { | |
1624 | my $addr = PMG::Utils::quote_regex($obj->{address}); | |
1625 | $fromlist->{"/^.+\@$addr\$/"} = 1; | |
1626 | } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') { | |
1627 | my $addr = $obj->{address}; | |
1628 | $addr =~ s|/|\\/|g; | |
1629 | $fromlist->{"/^$addr\$/"} = 1; | |
1630 | } elsif ($oclass eq 'PMG::RuleDB::IPAddress') { | |
1631 | $clientlist->{$obj->{address}} = 1; | |
1632 | } elsif ($oclass eq 'PMG::RuleDB::IPNet') { | |
1633 | $clientlist->{$obj->{address}} = 1; | |
1634 | } | |
1635 | } | |
1636 | ||
1637 | $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist); | |
1638 | $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist); | |
1639 | $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist); | |
1640 | $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit'); | |
1641 | }; | |
1642 | ||
1643 | # rewrite /etc/postfix/* | |
1644 | sub rewrite_config_postfix { | |
1645 | my ($self, $rulecache) = @_; | |
1646 | ||
1647 | # make sure we have required files (else postfix start fails) | |
1648 | IO::File->new($transport_map_filename, 'a', 0644); | |
1649 | ||
1650 | my $changes = 0; | |
1651 | ||
1652 | if ($self->get('mail', 'tls')) { | |
1653 | eval { | |
1654 | PMG::Utils::gen_proxmox_tls_cert(); | |
1655 | }; | |
1656 | syslog ('info', "generating certificate failed: $@") if $@; | |
1657 | } | |
1658 | ||
1659 | $changes = 1 if $self->rewrite_config_file( | |
1660 | 'main.cf.in', '/etc/postfix/main.cf'); | |
1661 | ||
1662 | $changes = 1 if $self->rewrite_config_file( | |
1663 | 'master.cf.in', '/etc/postfix/master.cf'); | |
1664 | ||
1665 | # make sure we have required files (else postfix start fails) | |
1666 | # Note: postmap need a valid /etc/postfix/main.cf configuration | |
1667 | postmap_pmg_domains(); | |
1668 | postmap_pmg_transport(); | |
1669 | postmap_tls_policy(); | |
1670 | ||
1671 | rewrite_postfix_whitelist($rulecache) if $rulecache; | |
1672 | ||
1673 | # make sure aliases.db is up to date | |
1674 | system('/usr/bin/newaliases'); | |
1675 | ||
1676 | return $changes; | |
1677 | } | |
1678 | ||
1679 | #parameters affecting services w/o config-file (pmgpolicy, pmg-smtp-filter) | |
1680 | my $pmg_service_params = { | |
1681 | mail => { | |
1682 | hide_received => 1, | |
1683 | ndr_on_block => 1, | |
1684 | }, | |
1685 | admin => { | |
1686 | dkim_selector => 1, | |
1687 | dkim_sign => 1, | |
1688 | dkim_sign_all_mail => 1, | |
1689 | }, | |
1690 | }; | |
1691 | ||
1692 | my $smtp_filter_cfg = '/run/pmg-smtp-filter.cfg'; | |
1693 | my $smtp_filter_cfg_lock = '/run/pmg-smtp-filter.cfg.lck'; | |
1694 | ||
1695 | sub dump_smtp_filter_config { | |
1696 | my ($self) = @_; | |
1697 | ||
1698 | my $conf = ''; | |
1699 | my $val; | |
1700 | foreach my $sec (sort keys %$pmg_service_params) { | |
1701 | my $conf_sec = $self->{ids}->{$sec} // {}; | |
1702 | foreach my $key (sort keys %{$pmg_service_params->{$sec}}) { | |
1703 | $val = $conf_sec->{$key}; | |
1704 | $conf .= "$sec.$key:$val\n" if defined($val); | |
1705 | } | |
1706 | } | |
1707 | ||
1708 | return $conf; | |
1709 | } | |
1710 | ||
1711 | sub compare_smtp_filter_config { | |
1712 | my ($self) = @_; | |
1713 | ||
1714 | my $ret = 0; | |
1715 | my $old; | |
1716 | eval { | |
1717 | $old = PVE::Tools::file_get_contents($smtp_filter_cfg); | |
1718 | }; | |
1719 | ||
1720 | if (my $err = $@) { | |
1721 | syslog ('warning', "reloading pmg-smtp-filter: $err"); | |
1722 | $ret = 1; | |
1723 | } else { | |
1724 | my $new = $self->dump_smtp_filter_config(); | |
1725 | $ret = 1 if $old ne $new; | |
1726 | } | |
1727 | ||
1728 | $self->write_smtp_filter_config() if $ret; | |
1729 | ||
1730 | return $ret; | |
1731 | } | |
1732 | ||
1733 | # writes the parameters relevant for pmg-smtp-filter to /run/ for comparison | |
1734 | # on config change | |
1735 | sub write_smtp_filter_config { | |
1736 | my ($self) = @_; | |
1737 | ||
1738 | PVE::Tools::lock_file($smtp_filter_cfg_lock, undef, sub { | |
1739 | PVE::Tools::file_set_contents($smtp_filter_cfg, | |
1740 | $self->dump_smtp_filter_config()); | |
1741 | }); | |
1742 | ||
1743 | die $@ if $@; | |
1744 | } | |
1745 | ||
1746 | sub rewrite_config { | |
1747 | my ($self, $rulecache, $restart_services, $force_restart) = @_; | |
1748 | ||
1749 | $force_restart = {} if ! $force_restart; | |
1750 | ||
1751 | my $log_restart = sub { | |
1752 | syslog ('info', "configuration change detected for '$_[0]', restarting"); | |
1753 | }; | |
1754 | ||
1755 | if (($self->rewrite_config_postfix($rulecache) && $restart_services) || | |
1756 | $force_restart->{postfix}) { | |
1757 | $log_restart->('postfix'); | |
1758 | PMG::Utils::service_cmd('postfix', 'reload'); | |
1759 | } | |
1760 | ||
1761 | if ($self->rewrite_dot_forward() && $restart_services) { | |
1762 | # no need to restart anything | |
1763 | } | |
1764 | ||
1765 | if ($self->rewrite_config_postgres() && $restart_services) { | |
1766 | # do nothing (too many side effects)? | |
1767 | # does not happen anyways, because config does not change. | |
1768 | } | |
1769 | ||
1770 | if (($self->rewrite_config_spam() && $restart_services) || | |
1771 | $force_restart->{spam}) { | |
1772 | $log_restart->('pmg-smtp-filter'); | |
1773 | PMG::Utils::service_cmd('pmg-smtp-filter', 'restart'); | |
1774 | } | |
1775 | ||
1776 | if (($self->rewrite_config_clam() && $restart_services) || | |
1777 | $force_restart->{clam}) { | |
1778 | $log_restart->('clamav-daemon'); | |
1779 | PMG::Utils::service_cmd('clamav-daemon', 'restart'); | |
1780 | } | |
1781 | ||
1782 | if (($self->rewrite_config_freshclam() && $restart_services) || | |
1783 | $force_restart->{freshclam}) { | |
1784 | $log_restart->('clamav-freshclam'); | |
1785 | PMG::Utils::service_cmd('clamav-freshclam', 'restart'); | |
1786 | } | |
1787 | ||
1788 | if (($self->compare_smtp_filter_config() && $restart_services) || | |
1789 | $force_restart->{spam}) { | |
1790 | syslog ('info', "scheduled reload for pmg-smtp-filter"); | |
1791 | PMG::Utils::reload_smtp_filter(); | |
1792 | } | |
1793 | } | |
1794 | ||
1795 | 1; |