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