blob: 88e66e8ef904c394d3a446354151f0cde681c9d4 [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2// File size is limited by Nginx site to 10M
3// To speed things up, we do not include prerequisites
4header('Content-Type: text/plain');
5require_once "vars.inc.php";
6// Do not show errors, we log to using error_log
7ini_set('error_reporting', 0);
8// Init database
9//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
10$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
11$opt = [
12 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
13 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
14 PDO::ATTR_EMULATE_PREPARES => false,
15];
16try {
17 $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
18}
19catch (PDOException $e) {
20 error_log("QUARANTINE: " . $e . PHP_EOL);
21 http_response_code(501);
22 exit;
23}
24// Init Redis
25$redis = new Redis();
26$redis->connect('redis-mailcow', 6379);
27
28// Functions
29function parse_email($email) {
30 if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
31 $a = strrpos($email, '@');
32 return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
33}
34if (!function_exists('getallheaders')) {
35 function getallheaders() {
36 if (!is_array($_SERVER)) {
37 return array();
38 }
39 $headers = array();
40 foreach ($_SERVER as $name => $value) {
41 if (substr($name, 0, 5) == 'HTTP_') {
42 $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
43 }
44 }
45 return $headers;
46 }
47}
48
49$raw_data_content = file_get_contents('php://input');
50$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
51$headers = getallheaders();
52
53$qid = $headers['X-Rspamd-Qid'];
54$fuzzy = $headers['X-Rspamd-Fuzzy'];
55$subject = $headers['X-Rspamd-Subject'];
56$score = $headers['X-Rspamd-Score'];
57$rcpts = $headers['X-Rspamd-Rcpt'];
58$user = $headers['X-Rspamd-User'];
59$ip = $headers['X-Rspamd-Ip'];
60$action = $headers['X-Rspamd-Action'];
61$sender = $headers['X-Rspamd-From'];
62$symbols = $headers['X-Rspamd-Symbols'];
63
64$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
65
66if (empty($sender)) {
67 error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
68 $sender = 'empty-env-from@localhost';
69}
70
71if ($fuzzy == 'unknown') {
72 $fuzzy = '[]';
73}
74
75try {
76 $max_size = (int)$redis->Get('Q_MAX_SIZE');
77 if (($max_size * 1048576) < $raw_size) {
78 error_log(sprintf("QUARANTINE: Message too large: %d b exceeds %d b", $raw_size, ($max_size * 1048576)) . PHP_EOL);
79 http_response_code(505);
80 exit;
81 }
82 if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) {
83 $exclude_domains = json_decode($exclude_domains, true);
84 }
85 $retention_size = (int)$redis->Get('Q_RETENTION_SIZE');
86}
87catch (RedisException $e) {
88 error_log("QUARANTINE: " . $e . PHP_EOL);
89 http_response_code(504);
90 exit;
91}
92
93$rcpt_final_mailboxes = array();
94
95// Loop through all rcpts
96foreach (json_decode($rcpts, true) as $rcpt) {
97 // Remove tag
98 $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
99
100 // Break rcpt into local part and domain part
101 $parsed_rcpt = parse_email($rcpt);
102
103 // Skip if not a mailcow handled domain
104 try {
105 if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
106 continue;
107 }
108 }
109 catch (RedisException $e) {
110 error_log("QUARANTINE: " . $e . PHP_EOL);
111 http_response_code(504);
112 exit;
113 }
114
115 // Skip if domain is excluded
116 if (in_array($parsed_rcpt['domain'], $exclude_domains)) {
117 error_log(sprintf("QUARANTINE: Skipped domain %s", $parsed_rcpt['domain']) . PHP_EOL);
118 continue;
119 }
120
121 // Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases
122 //
123 // rcpt
124 // |
125 // mailbox <-- goto ---> alias1, alias2, mailbox2
126 // | |
127 // mailbox3 |
128 // |
129 // alias3 ---> mailbox4
130 //
131 try {
132 $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
133 $stmt->execute(array(
134 ':rcpt' => $rcpt
135 ));
136 $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
137 if (empty($gotos)) {
138 $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
139 $stmt->execute(array(
140 ':rcpt' => '@' . $parsed_rcpt['domain']
141 ));
142 $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
143 }
144 if (empty($gotos)) {
145 $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'");
146 $stmt->execute(array(':rcpt' => $parsed_rcpt['domain']));
147 $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
148 if ($goto_branch) {
149 $gotos = $parsed_rcpt['local'] . '@' . $goto_branch;
150 }
151 }
152 $gotos_array = explode(',', $gotos);
153
154 $loop_c = 0;
155
156 while (count($gotos_array) != 0 && $loop_c <= 20) {
157
158 // Loop through all found gotos
159 foreach ($gotos_array as $index => &$goto) {
160 error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
161 $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
162 $stmt->execute(array(':goto' => $goto));
163 $username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
164 if (!empty($username)) {
165 error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
166 // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
167 if (!in_array($username, $rcpt_final_mailboxes)) {
168 $rcpt_final_mailboxes[] = $username;
169 }
170 }
171 else {
172 $parsed_goto = parse_email($goto);
173 if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
174 error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
175 }
176 else {
177 $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
178 $stmt->execute(array(':goto' => $goto));
179 $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
180 if ($goto_branch) {
181 error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
182 $goto_branch_array = explode(',', $goto_branch);
183 } else {
184 $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
185 $stmt->execute(array(':domain' => $parsed_goto['domain']));
186 $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
187 if ($goto_branch) {
188 error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
189 $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
190 }
191 }
192 }
193 }
194 // goto item was processed, unset
195 unset($gotos_array[$index]);
196 }
197
198 // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array
199 if (!empty($goto_branch_array)) {
200 $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array));
201 unset($goto_branch_array);
202 }
203
204 // Reindex array
205 $gotos_array = array_values($gotos_array);
206
207 // Force exit if loop cannot be solved
208 // Postfix does not allow for alias loops, so this should never happen.
209 $loop_c++;
210 error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
211 }
212 }
213 catch (PDOException $e) {
214 error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
215 http_response_code(502);
216 exit;
217 }
218}
219
220foreach ($rcpt_final_mailboxes as $rcpt_final) {
221 error_log("QUARANTINE: quarantine pipe: processing quarantine message for rcpt " . $rcpt_final . PHP_EOL);
222 try {
223 $stmt = $pdo->prepare("INSERT INTO `quarantine` (`qid`, `subject`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`, `fuzzy_hashes`)
224 VALUES (:qid, :subject, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action, :fuzzy_hashes)");
225 $stmt->execute(array(
226 ':qid' => $qid,
227 ':subject' => $subject,
228 ':score' => $score,
229 ':sender' => $sender,
230 ':rcpt' => $rcpt_final,
231 ':symbols' => $symbols,
232 ':user' => $user,
233 ':ip' => $ip,
234 ':msg' => $raw_data,
235 ':action' => $action,
236 ':fuzzy_hashes' => $fuzzy
237 ));
238 $stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN (
239 SELECT `id`
240 FROM (
241 SELECT `id`
242 FROM `quarantine`
243 WHERE `rcpt` = :rcpt2
244 ORDER BY id DESC
245 LIMIT :retention_size
246 ) x
247 );');
248 $stmt->execute(array(
249 ':rcpt' => $rcpt_final,
250 ':rcpt2' => $rcpt_final,
251 ':retention_size' => $retention_size
252 ));
253 }
254 catch (PDOException $e) {
255 error_log("QUARANTINE: " . $e->getMessage() . PHP_EOL);
256 http_response_code(503);
257 exit;
258 }
259}
260