diff --git a/data/conf/rspamd/dynmaps/settings.php b/data/conf/rspamd/dynmaps/settings.php index bcb82264..99a72a53 100644 --- a/data/conf/rspamd/dynmaps/settings.php +++ b/data/conf/rspamd/dynmaps/settings.php @@ -47,7 +47,7 @@ function ucl_rcpts($object, $type) { $local = parse_email($row['address'])['local']; $domain = parse_email($row['address'])['domain']; if (!empty($local) && !empty($domain)) { - $rcpt[] = '/' . $local . '\+.*' . $domain . '/i'; + $rcpt[] = '/' . $local . '[+].*' . $domain . '/i'; } $rcpt[] = $row['address']; } @@ -65,7 +65,7 @@ function ucl_rcpts($object, $type) { $local = parse_email($row['alias'])['local']; $domain = parse_email($row['alias'])['domain']; if (!empty($local) && !empty($domain)) { - $rcpt[] = '/' . $local . '\+.*' . $domain . '/i'; + $rcpt[] = '/' . $local . '[+].*' . $domain . '/i'; } $rcpt[] = $row['alias']; } @@ -74,7 +74,7 @@ function ucl_rcpts($object, $type) { $local = parse_email($row['object'])['local']; $domain = parse_email($row['object'])['domain']; if (!empty($local) && !empty($domain)) { - $rcpt[] = '/' . $local . '\+.*' . $domain . '/i'; + $rcpt[] = '/' . $local . '[+].*' . $domain . '/i'; } $rcpt[] = $object; } diff --git a/data/conf/rspamd/meta_exporter/pipe.php b/data/conf/rspamd/meta_exporter/pipe.php index 1075032c..31f5daac 100644 --- a/data/conf/rspamd/meta_exporter/pipe.php +++ b/data/conf/rspamd/meta_exporter/pipe.php @@ -77,36 +77,103 @@ catch (RedisException $e) { exit; } -$filtered_rcpts = array(); +$rcpt_final_mailboxes = array(); + +// Loop through all rcpts foreach (json_decode($rcpts, true) as $rcpt) { - $parsed_mail = parse_email($rcpt); - if (in_array($parsed_mail['domain'], $exclude_domains)) { - error_log(sprintf("Skipped domain %s", $parsed_mail['domain'])); + // Break rcpt into local part and domain part + $parsed_rcpt = parse_email($rcpt); + + // Skip if not a mailcow handled domain + try { + if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { + continue; + } + } + catch (RedisException $e) { + error_log($e); + http_response_code(504); + exit; + } + + // Skip if domain is excluded + if (in_array($parsed_rcpt['domain'], $exclude_domains)) { + error_log(sprintf("Skipped domain %s", $parsed_rcpt['domain'])); continue; } + + // Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases + // + // rcpt + // | + // mailbox <-- goto ---> alias1, alias2, mailbox2 + // | | + // mailbox3 | + // | + // alias3 ---> mailbox4 + // try { - $stmt = $pdo->prepare("SELECT `goto` FROM `alias` - WHERE - ( - `address` = :rcpt - OR - `address` IN ( - SELECT username FROM mailbox, alias_domain - WHERE (alias_domain.alias_domain = :domain_part - AND mailbox.username = CONCAT(:local_part, '@', alias_domain.target_domain) - AND mailbox.active = '1' - AND alias_domain.active='1') - ) - ) - AND `active`= '1';"); + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); $stmt->execute(array( - ':rcpt' => $rcpt, - ':local_part' => $parsed_mail['local'], - ':domain_part' => $parsed_mail['domain'] + ':rcpt' => $rcpt )); $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; - if (!empty($gotos)) { - $filtered_rcpts = array_unique(array_merge($filtered_rcpts, explode(',', $gotos))); + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => '@' . $parsed_rcpt['domain'] + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + } + $gotos_array = explode(',', $gotos); + + $loop_c = 0; + + while (count($gotos_array) != 0 && $loop_c <= 20) { + + // Loop through all found gotos + foreach ($gotos_array as $index => &$goto) { + error_log("quarantaine pipe: query " . $goto . " as username from mailbox"); + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND `active`= '1';"); + $stmt->execute(array(':goto' => $goto)); + $username = $stmt->fetch(PDO::FETCH_ASSOC)['username']; + if (!empty($username)) { + error_log("quarantaine pipe: mailbox found: " . $username); + // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate + if (!in_array($username, $rcpt_final_mailboxes)) { + $rcpt_final_mailboxes[] = $username; + } + } + else { + $parsed_goto = parse_email($goto); + if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { + error_log($goto . " is not a mailcow handled mailbox or alias address"); + } + else { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); + $stmt->execute(array(':goto' => $goto)); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + error_log("quarantaine pipe: goto address " . $goto . " is a alias branch for " . $goto_branch); + $goto_branch_array = explode(',', $goto_branch); + } + } + // goto item was processed, unset + unset($gotos_array[$index]); + } + + // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array + if (!empty($goto_branch_array)) { + $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array)); + unset($goto_branch_array); + } + + // Reindex array + $gotos_array = array_values($gotos_array); + + // Force exit if loop cannot be solved + // Postfix does not allow for alias loops, so this should never happen. + $loop_c++; + error_log("quarantaine pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array)); } } catch (PDOException $e) { @@ -115,8 +182,9 @@ foreach (json_decode($rcpts, true) as $rcpt) { exit; } } -foreach ($filtered_rcpts as $rcpt) { - + +foreach ($rcpt_final_mailboxes as $rcpt) { + error_log("quarantaine pipe: processing quarantaine message for rcpt " . $rcpt); try { $stmt = $pdo->prepare("INSERT INTO `quarantaine` (`qid`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`) VALUES (:qid, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action)"); @@ -131,18 +199,19 @@ foreach ($filtered_rcpts as $rcpt) { ':msg' => $raw_data, ':action' => $action )); - $stmt = $pdo->prepare('DELETE FROM `quarantaine` WHERE `id` NOT IN ( + $stmt = $pdo->prepare('DELETE FROM `quarantaine` WHERE `rcpt` = :rcpt AND `id` NOT IN ( SELECT `id` FROM ( SELECT `id` FROM `quarantaine` - WHERE `rcpt` = :rcpt + WHERE `rcpt` = :rcpt2 ORDER BY id DESC LIMIT :retention_size ) x );'); $stmt->execute(array( ':rcpt' => $rcpt, + ':rcpt2' => $rcpt, ':retention_size' => $retention_size )); } diff --git a/docker-compose.yml b/docker-compose.yml index 8eea2f8f..59bc7cd2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,7 +91,7 @@ services: - rspamd php-fpm-mailcow: - image: mailcow/phpfpm:1.7 + image: mailcow/phpfpm:1.8 build: ./data/Dockerfiles/phpfpm command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" depends_on: