blob: 274e9da285caecacf5f99315bc2639dc112f9537 [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2/*
3The match section performs AND operation on different matches: for example, if you have from and rcpt in the same rule,
4then the rule matches only when from AND rcpt match. For similar matches, the OR rule applies: if you have multiple rcpt matches,
5then any of these will trigger the rule. If a rule is triggered then no more rules are matched.
6*/
7header('Content-Type: text/plain');
8require_once "vars.inc.php";
9// Getting headers sent by the client.
10ini_set('error_reporting', 0);
11
12//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
13$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
14$opt = [
15 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
16 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
17 PDO::ATTR_EMULATE_PREPARES => false,
18];
19try {
20 $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
21 $stmt = $pdo->query("SELECT '1' FROM `filterconf`");
22}
23catch (PDOException $e) {
24 echo 'settings { }';
25 exit;
26}
27
28// Check if db changed and return header
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +010029$stmt = $pdo->prepare("SELECT GREATEST(COALESCE(MAX(UNIX_TIMESTAMP(UPDATE_TIME)), 1), COALESCE(MAX(UNIX_TIMESTAMP(CREATE_TIME)), 1)) AS `db_update_time` FROM `information_schema`.`tables`
30 WHERE (`TABLE_NAME` = 'filterconf' OR `TABLE_NAME` = 'settingsmap' OR `TABLE_NAME` = 'sogo_quick_contact' OR `TABLE_NAME` = 'alias')
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010031 AND TABLE_SCHEMA = :dbname;");
32$stmt->execute(array(
33 ':dbname' => $database_name
34));
35$db_update_time = $stmt->fetch(PDO::FETCH_ASSOC)['db_update_time'];
36if (empty($db_update_time)) {
37 $db_update_time = 1572048000;
38}
39if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && (strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $db_update_time)) {
40 header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 304);
41 exit;
42} else {
43 header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 200);
44}
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010045
46function parse_email($email) {
47 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
48 $a = strrpos($email, '@');
49 return array('local' => substr($email, 0, $a), 'domain' => substr($email, $a));
50}
51
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +010052function normalize_email($email) {
53 $email = strtolower(str_replace('/', '\/', $email));
54 $gm = "@gmail.com";
55 if (substr_compare($email, $gm, -strlen($gm)) == 0) {
56 $email = explode('@', $email);
57 $email[0] = str_replace('.', '', $email[0]);
58 $email = implode('@', $email);
59 }
60 $gm_alt = "@googlemail.com";
61 if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) {
62 $email = explode('@', $email);
63 $email[0] = str_replace('.', '', $email[0]);
64 $email[1] = str_replace('@', '', $gm);
65 $email = implode('@', $email);
66 }
67 if (str_contains($email, "+")) {
68 $email = explode('@', $email);
69 $user = explode('+', $email[0]);
70 $email[0] = $user[0];
71 $email = implode('@', $email);
72 }
73 return $email;
74}
75
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010076function wl_by_sogo() {
77 global $pdo;
78 $rcpt = array();
79 $stmt = $pdo->query("SELECT DISTINCT(`sogo_folder_info`.`c_path2`) AS `user`, GROUP_CONCAT(`sogo_quick_contact`.`c_mail`) AS `contacts` FROM `sogo_folder_info`
80 INNER JOIN `sogo_quick_contact` ON `sogo_quick_contact`.`c_folder_id` = `sogo_folder_info`.`c_folder_id`
81 GROUP BY `c_path2`");
82 $sogo_contacts = $stmt->fetchAll(PDO::FETCH_ASSOC);
83 while ($row = array_shift($sogo_contacts)) {
84 foreach (explode(',', $row['contacts']) as $contact) {
85 if (!filter_var($contact, FILTER_VALIDATE_EMAIL)) {
86 continue;
87 }
88 // Explicit from, no mime_from, no regex - envelope must match
89 // mailcow white and blacklists also cover mime_from
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +010090 $rcpt[$row['user']][] = normalize_email($contact);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010091 }
92 }
93 return $rcpt;
94}
95
96function ucl_rcpts($object, $type) {
97 global $pdo;
98 $rcpt = array();
99 if ($type == 'mailbox') {
100 // Standard aliases
101 $stmt = $pdo->prepare("SELECT `address` FROM `alias`
102 WHERE `goto` = :object_goto
103 AND `address` NOT LIKE '@%'");
104 $stmt->execute(array(
105 ':object_goto' => $object
106 ));
107 $standard_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
108 while ($row = array_shift($standard_aliases)) {
109 $local = parse_email($row['address'])['local'];
110 $domain = parse_email($row['address'])['domain'];
111 if (!empty($local) && !empty($domain)) {
112 $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
113 }
114 $rcpt[] = str_replace('/', '\/', $row['address']);
115 }
116 // Aliases by alias domains
117 $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
118 LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain`
119 WHERE `mailbox`.`username` = :object");
120 $stmt->execute(array(
121 ':object' => $object
122 ));
123 $by_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
124 array_filter($by_domain_aliases);
125 while ($row = array_shift($by_domain_aliases)) {
126 if (!empty($row['alias'])) {
127 $local = parse_email($row['alias'])['local'];
128 $domain = parse_email($row['alias'])['domain'];
129 if (!empty($local) && !empty($domain)) {
130 $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
131 }
132 $rcpt[] = str_replace('/', '\/', $row['alias']);
133 }
134 }
135 }
136 elseif ($type == 'domain') {
137 // Domain self
138 $rcpt[] = '/.*@' . $object . '/i';
139 $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
140 WHERE `target_domain` = :object");
141 $stmt->execute(array(':object' => $object));
142 $alias_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
143 array_filter($alias_domains);
144 while ($row = array_shift($alias_domains)) {
145 $rcpt[] = '/.*@' . $row['alias_domain'] . '/i';
146 }
147 }
148 return $rcpt;
149}
150?>
151settings {
152 watchdog {
153 priority = 10;
154 rcpt_mime = "/null@localhost/i";
155 from_mime = "/watchdog@localhost/i";
156 apply "default" {
157 symbols_disabled = ["HISTORY_SAVE", "ARC", "ARC_SIGNED", "DKIM", "DKIM_SIGNED", "CLAM_VIRUS"];
158 want_spam = yes;
159 actions {
160 reject = 9999.0;
161 greylist = 9998.0;
162 "add header" = 9997.0;
163 }
164
165 }
166 }
167<?php
168
169/*
170// Start custom scores for users
171*/
172
173$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'highspamlevel' OR `option` = 'lowspamlevel'");
174$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
175
176while ($row = array_shift($rows)) {
177 $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
178?>
179 score_<?=$username_sane;?> {
180 priority = 4;
181<?php
182 foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
183?>
184 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
185<?php
186 }
187 $stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
188 WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
189 AND `object`= :object");
190 $stmt->execute(array(':object' => $row['object']));
191 $spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP);
192?>
193 apply "default" {
194 actions {
195 reject = <?=$spamscore['highspamlevel'][0];?>;
196 greylist = <?=$spamscore['lowspamlevel'][0] - 1;?>;
197 "add header" = <?=$spamscore['lowspamlevel'][0];?>;
198 }
199 }
200 }
201<?php
202}
203
204/*
205// Start SOGo contacts whitelist
206// Priority 4, lower than a domain whitelist (5) and lower than a mailbox whitelist (6)
207*/
208
209foreach (wl_by_sogo() as $user => $contacts) {
210 $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $user);
211?>
212 whitelist_sogo_<?=$username_sane;?> {
213<?php
214 foreach ($contacts as $contact) {
215?>
216 from = <?=json_encode($contact, JSON_UNESCAPED_SLASHES);?>;
217<?php
218 }
219?>
220 priority = 4;
221<?php
222 foreach (ucl_rcpts($user, 'mailbox') as $rcpt) {
223?>
224 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
225<?php
226 }
227?>
228 apply "default" {
229 SOGO_CONTACT = -99.0;
230 }
231 symbols [
232 "SOGO_CONTACT"
233 ]
234 }
235<?php
236}
237
238/*
239// Start whitelist
240*/
241
242$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'whitelist_from'");
243$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
244while ($row = array_shift($rows)) {
245 $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
246?>
247 whitelist_<?=$username_sane;?> {
248<?php
249 $list_items = array();
250 $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
251 WHERE `object`= :object
252 AND `option` = 'whitelist_from'");
253 $stmt->execute(array(':object' => $row['object']));
254 $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
255 foreach ($list_items as $item) {
256?>
257 from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
258<?php
259 }
260 if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
261?>
262 priority = 5;
263<?php
264 foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
265?>
266 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
267<?php
268 }
269 }
270 else {
271?>
272 priority = 6;
273<?php
274 foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
275?>
276 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
277<?php
278 }
279 }
280?>
281 apply "default" {
282 MAILCOW_WHITE = -999.0;
283 }
284 symbols [
285 "MAILCOW_WHITE"
286 ]
287 }
288 whitelist_mime_<?=$username_sane;?> {
289<?php
290 foreach ($list_items as $item) {
291?>
292 from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
293<?php
294 }
295 if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
296?>
297 priority = 5;
298<?php
299 foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
300?>
301 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
302<?php
303 }
304 }
305 else {
306?>
307 priority = 6;
308<?php
309 foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
310?>
311 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
312<?php
313 }
314 }
315?>
316 apply "default" {
317 MAILCOW_WHITE = -999.0;
318 }
319 symbols [
320 "MAILCOW_WHITE"
321 ]
322 }
323<?php
324}
325
326/*
327// Start blacklist
328*/
329
330$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'blacklist_from'");
331$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
332while ($row = array_shift($rows)) {
333 $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
334?>
335 blacklist_<?=$username_sane;?> {
336<?php
337 $list_items = array();
338 $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
339 WHERE `object`= :object
340 AND `option` = 'blacklist_from'");
341 $stmt->execute(array(':object' => $row['object']));
342 $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
343 foreach ($list_items as $item) {
344?>
345 from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
346<?php
347 }
348 if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
349?>
350 priority = 5;
351<?php
352 foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
353?>
354 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
355<?php
356 }
357 }
358 else {
359?>
360 priority = 6;
361<?php
362 foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
363?>
364 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
365<?php
366 }
367 }
368?>
369 apply "default" {
370 MAILCOW_BLACK = 999.0;
371 }
372 symbols [
373 "MAILCOW_BLACK"
374 ]
375 }
376 blacklist_header_<?=$username_sane;?> {
377<?php
378 foreach ($list_items as $item) {
379?>
380 from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
381<?php
382 }
383 if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
384?>
385 priority = 5;
386<?php
387 foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
388?>
389 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
390<?php
391 }
392 }
393 else {
394?>
395 priority = 6;
396<?php
397 foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
398?>
399 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
400<?php
401 }
402 }
403?>
404 apply "default" {
405 MAILCOW_BLACK = 999.0;
406 }
407 symbols [
408 "MAILCOW_BLACK"
409 ]
410 }
411<?php
412}
413
414/*
415// Start traps
416*/
417
418?>
419 ham_trap {
420<?php
421 foreach (ucl_rcpts('ham@localhost', 'mailbox') as $rcpt) {
422?>
423 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
424<?php
425 }
426?>
427 priority = 9;
428 apply "default" {
429 symbols_enabled = ["HISTORY_SAVE"];
430 }
431 symbols [
432 "HAM_TRAP"
433 ]
434 }
435
436 spam_trap {
437<?php
438 foreach (ucl_rcpts('spam@localhost', 'mailbox') as $rcpt) {
439?>
440 rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
441<?php
442 }
443?>
444 priority = 9;
445 apply "default" {
446 symbols_enabled = ["HISTORY_SAVE"];
447 }
448 symbols [
449 "SPAM_TRAP"
450 ]
451 }
452<?php
453// Start additional content
454
455$stmt = $pdo->query("SELECT `id`, `content` FROM `settingsmap` WHERE `active` = '1'");
456$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
457while ($row = array_shift($rows)) {
458 $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['id']);
459?>
460 additional_settings_<?=intval($row['id']);?> {
461<?php
462 $content = preg_split('/\r\n|\r|\n/', $row['content']);
463 foreach ($content as $line) {
464 echo ' ' . $line . PHP_EOL;
465 }
466?>
467 }
468<?php
469}
470?>
471}