blob: c89b3e408efe9870d1b977c922bf0d8ea9e57f8f [file] [log] [blame]
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +01001<?php
2use PHPMailer\PHPMailer\PHPMailer;
3use PHPMailer\PHPMailer\SMTP;
4use PHPMailer\PHPMailer\Exception;
5function quarantine($_action, $_data = null) {
6 global $pdo;
7 global $redis;
8 global $lang;
9 $_data_log = $_data;
10 switch ($_action) {
11 case 'quick_delete':
12 // Dont return results, just log
13 $hash = trim($_data);
14 if (preg_match("/^([a-f0-9]{64})$/", $hash) === false) {
15 logger(array('return' => array(
16 array(
17 'type' => 'danger',
18 'log' => array(__FUNCTION__, $_action, $_data_log),
19 'msg' => 'access_denied'
20 )
21 )));
22 return false;
23 }
24 $stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +020025 WHERE `qhash` = :hash
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +010026 AND user_acl.quarantine = 1
27 AND rcpt IN (SELECT username FROM mailbox)');
28 $stmt->execute(array(':hash' => $hash));
29 $row = $stmt->fetch(PDO::FETCH_ASSOC);
30 if (empty($row['id']) || !is_numeric($row['id'])) {
31 logger(array('return' => array(
32 array(
33 'type' => 'danger',
34 'log' => array(__FUNCTION__, $_action, $_data_log),
35 'msg' => 'access_denied'
36 )
37 )));
38 return false;
39 }
40 else {
41 $stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE id = :id");
42 $stmt->execute(array(
43 ':id' => $row['id']
44 ));
45 }
46 logger(array('return' => array(
47 array(
48 'type' => 'success',
49 'log' => array(__FUNCTION__, $_action, $_data_log),
50 'msg' => array('item_deleted', $row['id'])
51 )
52 )));
53 break;
54 case 'quick_release':
55 // Dont return results, just log
56 $hash = trim($_data);
57 if (preg_match("/^([a-f0-9]{64})$/", $hash) === false) {
58 logger(array('return' => array(
59 array(
60 'type' => 'danger',
61 'log' => array(__FUNCTION__, $_action, $_data_log),
62 'msg' => 'access_denied'
63 )
64 )));
65 return false;
66 }
67 $stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +020068 WHERE `qhash` = :hash
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +010069 AND `user_acl`.`quarantine` = 1
70 AND `username` IN (SELECT `username` FROM `mailbox`)');
71 $stmt->execute(array(':hash' => $hash));
72 $row = $stmt->fetch(PDO::FETCH_ASSOC);
73 if (empty($row['id']) || !is_numeric($row['id'])) {
74 logger(array('return' => array(
75 array(
76 'type' => 'danger',
77 'log' => array(__FUNCTION__, $_action, $_data_log),
78 'msg' => 'access_denied'
79 )
80 )));
81 return false;
82 }
83 else {
84 $stmt = $pdo->prepare('SELECT `msg`, `qid`, `sender`, `rcpt` FROM `quarantine` WHERE `id` = :id');
85 $stmt->execute(array(':id' => $row['id']));
86 $detail_row = $stmt->fetch(PDO::FETCH_ASSOC);
87 $sender = !empty($detail_row['sender']) ? $detail_row['sender'] : 'sender-unknown@rspamd';
88 if (!empty(gethostbynamel('postfix-mailcow'))) {
89 $postfix = 'postfix-mailcow';
90 }
91 if (!empty(gethostbynamel('postfix'))) {
92 $postfix = 'postfix';
93 }
94 else {
95 logger(array('return' => array(
96 array(
97 'type' => 'warning',
98 'log' => array(__FUNCTION__, $_action, $_data_log),
99 'msg' => array('release_send_failed', 'Cannot determine Postfix host')
100 )
101 )));
102 return false;
103 }
104 try {
105 $release_format = $redis->Get('Q_RELEASE_FORMAT');
106 }
107 catch (RedisException $e) {
108 logger(array('return' => array(
109 array(
110 'type' => 'danger',
111 'log' => array(__FUNCTION__, $_action, $_data_log),
112 'msg' => array('redis_error', $e)
113 )
114 )));
115 return false;
116 }
117 if ($release_format == 'attachment') {
118 try {
119 $mail = new PHPMailer(true);
120 $mail->isSMTP();
121 $mail->SMTPDebug = 0;
122 $mail->SMTPOptions = array(
123 'ssl' => array(
124 'verify_peer' => false,
125 'verify_peer_name' => false,
126 'allow_self_signed' => true
127 )
128 );
129 if (!empty(gethostbynamel('postfix-mailcow'))) {
130 $postfix = 'postfix-mailcow';
131 }
132 if (!empty(gethostbynamel('postfix'))) {
133 $postfix = 'postfix';
134 }
135 else {
136 logger(array('return' => array(
137 array(
138 'type' => 'warning',
139 'log' => array(__FUNCTION__, $_action, $_data_log),
140 'msg' => array('release_send_failed', 'Cannot determine Postfix host')
141 )
142 )));
143 return false;
144 }
145 $mail->Host = $postfix;
146 $mail->Port = 590;
147 $mail->setFrom($sender);
148 $mail->CharSet = 'UTF-8';
149 $mail->Subject = sprintf($lang['quarantine']['release_subject'], $detail_row['qid']);
150 $mail->addAddress($detail_row['rcpt']);
151 $mail->IsHTML(false);
152 $msg_tmpf = tempnam("/tmp", $detail_row['qid']);
153 file_put_contents($msg_tmpf, $detail_row['msg']);
154 $mail->addAttachment($msg_tmpf, $detail_row['qid'] . '.eml');
155 $mail->Body = sprintf($lang['quarantine']['release_body']);
156 $mail->send();
157 unlink($msg_tmpf);
158 }
159 catch (phpmailerException $e) {
160 unlink($msg_tmpf);
161 logger(array('return' => array(
162 array(
163 'type' => 'warning',
164 'log' => array(__FUNCTION__, $_action, $_data_log),
165 'msg' => array('release_send_failed', $e->errorMessage())
166 )
167 )));
168 return false;
169 }
170 }
171 elseif ($release_format == 'raw') {
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +0200172 $detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
173 // dot-stuffing per RFC5321 4.5.2: escape leading dots
174 $detail_row['msg'] = preg_replace('~^\.~m', '..', $detail_row['msg']);
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +0100175 $postfix_talk = array(
176 array('220', 'HELO quarantine' . chr(10)),
177 array('250', 'MAIL FROM: ' . $sender . chr(10)),
178 array('250', 'RCPT TO: ' . $detail_row['rcpt'] . chr(10)),
179 array('250', 'DATA' . chr(10)),
180 array('354', $detail_row['msg'] . chr(10) . '.' . chr(10)),
181 array('250', 'QUIT' . chr(10)),
182 array('221', '')
183 );
184 // Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +0200185 $smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +0100186 if (!$smtp_connection) {
187 logger(array('return' => array(
188 array(
189 'type' => 'warning',
190 'log' => array(__FUNCTION__, $_action, $_data_log),
191 'msg' => 'Cannot connect to Postfix'
192 )
193 )));
194 return false;
195 }
196 for ($i=0; $i < count($postfix_talk); $i++) {
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +0200197 $smtp_resource = fgets($smtp_connection, 256);
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +0100198 if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
199 $ret = substr($smtp_resource, 0, 3);
200 $ret = (empty($ret)) ? '-' : $ret;
201 logger(array('return' => array(
202 array(
203 'type' => 'warning',
204 'log' => array(__FUNCTION__, $_action, $_data_log),
205 'msg' => 'Postfix returned SMTP code ' . $smtp_resource . ', expected ' . $postfix_talk[$i][0]
206 )
207 )));
208 return false;
209 }
210 if ($postfix_talk[$i][1] !== '') {
211 fputs($smtp_connection, $postfix_talk[$i][1]);
212 }
213 }
214 fclose($smtp_connection);
215 }
216 $stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE id = :id");
217 $stmt->execute(array(
218 ':id' => $row['id']
219 ));
220 }
221 logger(array('return' => array(
222 array(
223 'type' => 'success',
224 'log' => array(__FUNCTION__, $_action, $_data_log),
225 'msg' => array('item_released', $hash)
226 )
227 )));
228 break;
229 case 'delete':
230 if (!is_array($_data['id'])) {
231 $ids = array();
232 $ids[] = $_data['id'];
233 }
234 else {
235 $ids = $_data['id'];
236 }
237 if (!isset($_SESSION['acl']['quarantine']) || $_SESSION['acl']['quarantine'] != "1" ) {
238 $_SESSION['return'][] = array(
239 'type' => 'danger',
240 'log' => array(__FUNCTION__, $_action, $_data_log),
241 'msg' => 'access_denied'
242 );
243 return false;
244 }
245 foreach ($ids as $id) {
246 if (!is_numeric($id)) {
247 $_SESSION['return'][] = array(
248 'type' => 'danger',
249 'log' => array(__FUNCTION__, $_action, $_data_log),
250 'msg' => 'access_denied'
251 );
252 continue;
253 }
254 $stmt = $pdo->prepare('SELECT `rcpt` FROM `quarantine` WHERE `id` = :id');
255 $stmt->execute(array(':id' => $id));
256 $row = $stmt->fetch(PDO::FETCH_ASSOC);
257 if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt']) && $_SESSION['mailcow_cc_role'] != 'admin') {
258 $_SESSION['return'][] = array(
259 'type' => 'danger',
260 'log' => array(__FUNCTION__, $_action, $_data_log),
261 'msg' => 'access_denied'
262 );
263 continue;
264 }
265 else {
266 $stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `id` = :id");
267 $stmt->execute(array(
268 ':id' => $id
269 ));
270 }
271 $_SESSION['return'][] = array(
272 'type' => 'success',
273 'log' => array(__FUNCTION__, $_action, $_data_log),
274 'msg' => array('item_deleted', $id)
275 );
276 }
277 break;
278 case 'edit':
279 if (!isset($_SESSION['acl']['quarantine']) || $_SESSION['acl']['quarantine'] != "1" ) {
280 $_SESSION['return'][] = array(
281 'type' => 'danger',
282 'log' => array(__FUNCTION__, $_action, $_data_log),
283 'msg' => 'access_denied'
284 );
285 return false;
286 }
287 // Edit settings
288 if ($_data['action'] == 'settings') {
289 if ($_SESSION['mailcow_cc_role'] != "admin") {
290 $_SESSION['return'][] = array(
291 'type' => 'danger',
292 'log' => array(__FUNCTION__, $_action, $_data_log),
293 'msg' => 'access_denied'
294 );
295 return false;
296 }
297 $retention_size = $_data['retention_size'];
298 if ($_data['release_format'] == 'attachment' || $_data['release_format'] == 'raw') {
299 $release_format = $_data['release_format'];
300 }
301 else {
302 $release_format = 'raw';
303 }
304 $max_size = $_data['max_size'];
305 if ($_data['max_score'] == "") {
306 $max_score = '';
307 }
308 else {
309 $max_score = floatval($_data['max_score']);
310 }
311 $max_age = intval($_data['max_age']);
312 $subject = $_data['subject'];
313 if (!filter_var($_data['bcc'], FILTER_VALIDATE_EMAIL)) {
314 $bcc = '';
315 }
316 else {
317 $bcc = $_data['bcc'];
318 }
319 if (!filter_var($_data['redirect'], FILTER_VALIDATE_EMAIL)) {
320 $redirect = '';
321 }
322 else {
323 $redirect = $_data['redirect'];
324 }
325 if (!filter_var($_data['sender'], FILTER_VALIDATE_EMAIL)) {
326 $sender = '';
327 }
328 else {
329 $sender = $_data['sender'];
330 }
331 $html = $_data['html_tmpl'];
332 if ($max_age <= 0) {
333 $max_age = 365;
334 }
335 $exclude_domains = (array)$_data['exclude_domains'];
336 try {
337 $redis->Set('Q_RETENTION_SIZE', intval($retention_size));
338 $redis->Set('Q_MAX_SIZE', intval($max_size));
339 $redis->Set('Q_MAX_SCORE', $max_score);
340 $redis->Set('Q_MAX_AGE', $max_age);
341 $redis->Set('Q_EXCLUDE_DOMAINS', json_encode($exclude_domains));
342 $redis->Set('Q_RELEASE_FORMAT', $release_format);
343 $redis->Set('Q_SENDER', $sender);
344 $redis->Set('Q_BCC', $bcc);
345 $redis->Set('Q_REDIRECT', $redirect);
346 $redis->Set('Q_SUBJ', $subject);
347 $redis->Set('Q_HTML', $html);
348 }
349 catch (RedisException $e) {
350 $_SESSION['return'][] = array(
351 'type' => 'danger',
352 'log' => array(__FUNCTION__, $_action, $_data_log),
353 'msg' => array('redis_error', $e)
354 );
355 return false;
356 }
357 $_SESSION['return'][] = array(
358 'type' => 'success',
359 'log' => array(__FUNCTION__, $_action, $_data_log),
360 'msg' => 'saved_settings'
361 );
362 }
363 // Release item
364 elseif ($_data['action'] == 'release' || $_data['action'] == 'learnham') {
365 if (!is_array($_data['id'])) {
366 $ids = array();
367 $ids[] = $_data['id'];
368 }
369 else {
370 $ids = $_data['id'];
371 }
372 foreach ($ids as $id) {
373 if (!is_numeric($id)) {
374 $_SESSION['return'][] = array(
375 'type' => 'danger',
376 'log' => array(__FUNCTION__, $_action, $_data_log),
377 'msg' => 'access_denied'
378 );
379 continue;
380 }
381 $stmt = $pdo->prepare('SELECT `msg`, `action`, `qid`, `sender`, `rcpt` FROM `quarantine` WHERE `id` = :id');
382 $stmt->execute(array(':id' => $id));
383 $row = $stmt->fetch(PDO::FETCH_ASSOC);
384 if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt']) && $_SESSION['mailcow_cc_role'] != 'admin' || empty($row['rcpt'])) {
385 $_SESSION['return'][] = array(
386 'type' => 'danger',
387 'log' => array(__FUNCTION__, $_action, $_data_log),
388 'msg' => 'access_denied'
389 );
390 continue;
391 }
392 $sender = !empty($row['sender']) ? $row['sender'] : 'sender-unknown@rspamd';
393 if (!empty(gethostbynamel('postfix-mailcow'))) {
394 $postfix = 'postfix-mailcow';
395 }
396 if (!empty(gethostbynamel('postfix'))) {
397 $postfix = 'postfix';
398 }
399 else {
400 $_SESSION['return'][] = array(
401 'type' => 'warning',
402 'log' => array(__FUNCTION__, $_action, $_data_log),
403 'msg' => array('release_send_failed', 'Cannot determine Postfix host')
404 );
405 continue;
406 }
407 try {
408 $release_format = $redis->Get('Q_RELEASE_FORMAT');
409 }
410 catch (RedisException $e) {
411 $_SESSION['return'][] = array(
412 'type' => 'danger',
413 'log' => array(__FUNCTION__, $_action, $_data_log),
414 'msg' => array('redis_error', $e)
415 );
416 return false;
417 }
418 if ($release_format == 'attachment') {
419 try {
420 $mail = new PHPMailer(true);
421 $mail->isSMTP();
422 $mail->SMTPDebug = 0;
423 $mail->SMTPOptions = array(
424 'ssl' => array(
425 'verify_peer' => false,
426 'verify_peer_name' => false,
427 'allow_self_signed' => true
428 )
429 );
430 if (!empty(gethostbynamel('postfix-mailcow'))) {
431 $postfix = 'postfix-mailcow';
432 }
433 if (!empty(gethostbynamel('postfix'))) {
434 $postfix = 'postfix';
435 }
436 else {
437 $_SESSION['return'][] = array(
438 'type' => 'warning',
439 'log' => array(__FUNCTION__, $_action, $_data_log),
440 'msg' => array('release_send_failed', 'Cannot determine Postfix host')
441 );
442 continue;
443 }
444 $mail->Host = $postfix;
445 $mail->Port = 590;
446 $mail->setFrom($sender);
447 $mail->CharSet = 'UTF-8';
448 $mail->Subject = sprintf($lang['quarantine']['release_subject'], $row['qid']);
449 $mail->addAddress($row['rcpt']);
450 $mail->IsHTML(false);
451 $msg_tmpf = tempnam("/tmp", $row['qid']);
452 file_put_contents($msg_tmpf, $row['msg']);
453 $mail->addAttachment($msg_tmpf, $row['qid'] . '.eml');
454 $mail->Body = sprintf($lang['quarantine']['release_body']);
455 $mail->send();
456 unlink($msg_tmpf);
457 }
458 catch (phpmailerException $e) {
459 unlink($msg_tmpf);
460 $_SESSION['return'][] = array(
461 'type' => 'warning',
462 'log' => array(__FUNCTION__, $_action, $_data_log),
463 'msg' => array('release_send_failed', $e->errorMessage())
464 );
465 continue;
466 }
467 }
468 elseif ($release_format == 'raw') {
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +0200469 $row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
470 // dot-stuffing per RFC5321 4.5.2: escape leading dots
471 $row['msg'] = preg_replace('~^\.~m', '..', $row['msg']);
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +0100472 $postfix_talk = array(
473 array('220', 'HELO quarantine' . chr(10)),
474 array('250', 'MAIL FROM: ' . $sender . chr(10)),
475 array('250', 'RCPT TO: ' . $row['rcpt'] . chr(10)),
476 array('250', 'DATA' . chr(10)),
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +0200477 array('354', $row['msg'] . chr(10) . '.' . chr(10)),
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +0100478 array('250', 'QUIT' . chr(10)),
479 array('221', '')
480 );
481 // Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +0200482 $smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +0100483 if (!$smtp_connection) {
484 $_SESSION['return'][] = array(
485 'type' => 'warning',
486 'log' => array(__FUNCTION__, $_action, $_data_log),
487 'msg' => 'Cannot connect to Postfix'
488 );
489 return false;
490 }
491 for ($i=0; $i < count($postfix_talk); $i++) {
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +0200492 $smtp_resource = fgets($smtp_connection, 256);
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +0100493 if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
494 $ret = substr($smtp_resource, 0, 3);
495 $ret = (empty($ret)) ? '-' : $ret;
496 $_SESSION['return'][] = array(
497 'type' => 'warning',
498 'log' => array(__FUNCTION__, $_action, $_data_log),
499 'msg' => 'Postfix returned SMTP code ' . $smtp_resource . ', expected ' . $postfix_talk[$i][0]
500 );
501 return false;
502 }
503 if ($postfix_talk[$i][1] !== '') {
504 fputs($smtp_connection, $postfix_talk[$i][1]);
505 }
506 }
507 fclose($smtp_connection);
508 }
509 $stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `id` = :id");
510 $stmt->execute(array(
511 ':id' => $id
512 ));
513 $_SESSION['return'][] = array(
514 'type' => 'success',
515 'log' => array(__FUNCTION__, $_action, $_data_log),
516 'msg' => array('item_released', $id)
517 );
518 // Item was released and deleted from quarantine, now learning ham
519 $curl = curl_init();
520 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
521 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
522 curl_setopt($curl, CURLOPT_POST, 1);
523 curl_setopt($curl, CURLOPT_TIMEOUT, 30);
524 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain'));
525 curl_setopt($curl, CURLOPT_URL,"http://rspamd/learnham");
526 curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
527 $response = curl_exec($curl);
528 if (!curl_errno($curl)) {
529 $response = json_decode($response, true);
530 if (isset($response['error'])) {
531 if (stripos($response['error'], 'already learned') === false) {
532 $_SESSION['return'][] = array(
533 'type' => 'danger',
534 'log' => array(__FUNCTION__, $_action, $_data_log),
535 'msg' => array('ham_learn_error', $response['error'])
536 );
537 continue;
538 }
539 }
540 curl_close($curl);
541 $curl = curl_init();
542 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
543 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
544 curl_setopt($curl, CURLOPT_POST, 1);
545 curl_setopt($curl, CURLOPT_TIMEOUT, 30);
546 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 11'));
547 curl_setopt($curl, CURLOPT_URL,"http://rspamd/fuzzydel");
548 curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
549 // It is most likely not a spam hash, so we ignore any error/warning response
550 // $response = curl_exec($curl);
551 if (!curl_errno($curl)) {
552 curl_close($curl);
553 $curl = curl_init();
554 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
555 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
556 curl_setopt($curl, CURLOPT_POST, 1);
557 curl_setopt($curl, CURLOPT_TIMEOUT, 30);
558 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 13'));
559 curl_setopt($curl, CURLOPT_URL,"http://rspamd/fuzzyadd");
560 curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
561 $response = curl_exec($curl);
562 curl_exec($curl);
563 if (!curl_errno($curl)) {
564 $response = json_decode($response, true);
565 if (isset($response['error'])) {
566 if (stripos($response['error'], 'No content to generate fuzzy') === false) {
567 $_SESSION['return'][] = array(
568 'type' => 'warning',
569 'log' => array(__FUNCTION__, $_action, $_data_log),
570 'msg' => array('fuzzy_learn_error', $response['error'])
571 );
572 }
573 }
574 }
575 curl_close($curl);
576 $_SESSION['return'][] = array(
577 'type' => 'success',
578 'log' => array(__FUNCTION__, $_action, $_data_log),
579 'msg' => array('learned_ham', $id)
580 );
581 continue;
582 }
583 else {
584 curl_close($curl);
585 $_SESSION['return'][] = array(
586 'type' => 'danger',
587 'log' => array(__FUNCTION__, $_action, $_data_log),
588 'msg' => array('ham_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
589 );
590 continue;
591 }
592 curl_close($curl);
593 $_SESSION['return'][] = array(
594 'type' => 'danger',
595 'log' => array(__FUNCTION__, $_action, $_data_log),
596 'msg' => array('ham_learn_error', 'unknown')
597 );
598 continue;
599 }
600 else {
601 $_SESSION['return'][] = array(
602 'type' => 'danger',
603 'log' => array(__FUNCTION__, $_action, $_data_log),
604 'msg' => array('ham_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
605 );
606 curl_close($curl);
607 continue;
608 }
609 curl_close($curl);
610 $_SESSION['return'][] = array(
611 'type' => 'danger',
612 'log' => array(__FUNCTION__, $_action, $_data_log),
613 'msg' => array('ham_learn_error', 'unknown')
614 );
615 continue;
616 }
617 }
618 elseif ($_data['action'] == 'learnspam') {
619 if (!is_array($_data['id'])) {
620 $ids = array();
621 $ids[] = $_data['id'];
622 }
623 else {
624 $ids = $_data['id'];
625 }
626 foreach ($ids as $id) {
627 if (!is_numeric($id)) {
628 $_SESSION['return'][] = array(
629 'type' => 'danger',
630 'log' => array(__FUNCTION__, $_action, $_data_log),
631 'msg' => 'access_denied'
632 );
633 continue;
634 }
635 $stmt = $pdo->prepare('SELECT `msg`, `rcpt`, `action` FROM `quarantine` WHERE `id` = :id');
636 $stmt->execute(array(':id' => $id));
637 $row = $stmt->fetch(PDO::FETCH_ASSOC);
638 if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt']) && $_SESSION['mailcow_cc_role'] != 'admin' || empty($row['rcpt'])) {
639 $_SESSION['return'][] = array(
640 'type' => 'danger',
641 'log' => array(__FUNCTION__, $_action, $_data_log),
642 'msg' => 'access_denied'
643 );
644 continue;
645 }
646 $stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `id` = :id");
647 $stmt->execute(array(
648 ':id' => $id
649 ));
650 $curl = curl_init();
651 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
652 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
653 curl_setopt($curl, CURLOPT_POST, 1);
654 curl_setopt($curl, CURLOPT_TIMEOUT, 30);
655 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain'));
656 curl_setopt($curl, CURLOPT_URL,"http://rspamd/learnspam");
657 curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
658 $response = curl_exec($curl);
659 if (!curl_errno($curl)) {
660 $response = json_decode($response, true);
661 if (isset($response['error'])) {
662 if (stripos($response['error'], 'already learned') === false) {
663 $_SESSION['return'][] = array(
664 'type' => 'danger',
665 'log' => array(__FUNCTION__, $_action, $_data_log),
666 'msg' => array('spam_learn_error', $response['error'])
667 );
668 continue;
669 }
670 }
671 curl_close($curl);
672 $curl = curl_init();
673 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
674 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
675 curl_setopt($curl, CURLOPT_POST, 1);
676 curl_setopt($curl, CURLOPT_TIMEOUT, 30);
677 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 13'));
678 curl_setopt($curl, CURLOPT_URL,"http://rspamd/fuzzydel");
679 curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
680 // It is most likely not a spam hash, so we ignore any error/warning response
681 // $response = curl_exec($curl);
682 if (!curl_errno($curl)) {
683 curl_close($curl);
684 $curl = curl_init();
685 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
686 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
687 curl_setopt($curl, CURLOPT_POST, 1);
688 curl_setopt($curl, CURLOPT_TIMEOUT, 30);
689 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 11'));
690 curl_setopt($curl, CURLOPT_URL,"http://rspamd/fuzzyadd");
691 curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
692 $response = curl_exec($curl);
693 curl_exec($curl);
694 if (!curl_errno($curl)) {
695 $response = json_decode($response, true);
696 if (isset($response['error'])) {
697 if (stripos($response['error'], 'No content to generate fuzzy') === false) {
698 $_SESSION['return'][] = array(
699 'type' => 'warning',
700 'log' => array(__FUNCTION__, $_action, $_data_log),
701 'msg' => array('fuzzy_learn_error', $response['error'])
702 );
703 }
704 }
705 }
706 curl_close($curl);
707 $_SESSION['return'][] = array(
708 'type' => 'success',
709 'log' => array(__FUNCTION__, $_action, $_data_log),
710 'msg' => array('qlearn_spam', $id)
711 );
712 continue;
713 }
714 else {
715 curl_close($curl);
716 $_SESSION['return'][] = array(
717 'type' => 'danger',
718 'log' => array(__FUNCTION__, $_action, $_data_log),
719 'msg' => array('spam_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
720 );
721 continue;
722 }
723 curl_close($curl);
724 $_SESSION['return'][] = array(
725 'type' => 'danger',
726 'log' => array(__FUNCTION__, $_action, $_data_log),
727 'msg' => array('spam_learn_error', 'unknown')
728 );
729 continue;
730 }
731 else {
732 $_SESSION['return'][] = array(
733 'type' => 'danger',
734 'log' => array(__FUNCTION__, $_action, $_data_log),
735 'msg' => array('spam_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
736 );
737 curl_close($curl);
738 continue;
739 }
740 curl_close($curl);
741 $_SESSION['return'][] = array(
742 'type' => 'danger',
743 'log' => array(__FUNCTION__, $_action, $_data_log),
744 'msg' => array('spam_learn_error', 'unknown')
745 );
746 continue;
747 }
748 }
749 return true;
750 break;
751 case 'get':
752 if ($_SESSION['mailcow_cc_role'] == "user") {
753 $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, `action`, UNIX_TIMESTAMP(`created`) AS `created`, `notified` FROM `quarantine` WHERE `rcpt` = :mbox');
754 $stmt->execute(array(':mbox' => $_SESSION['mailcow_cc_username']));
755 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
756 while($row = array_shift($rows)) {
757 $q_meta[] = $row;
758 }
759 }
760 elseif ($_SESSION['mailcow_cc_role'] == "admin") {
761 $stmt = $pdo->query('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, `action`, UNIX_TIMESTAMP(`created`) AS `created`, `notified` FROM `quarantine`');
762 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
763 while($row = array_shift($rows)) {
764 $q_meta[] = $row;
765 }
766 }
767 else {
768 $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
769 foreach ($domains as $domain) {
770 $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, `action`, UNIX_TIMESTAMP(`created`) AS `created`, `notified` FROM `quarantine` WHERE `rcpt` REGEXP :domain');
771 $stmt->execute(array(':domain' => '@' . $domain . '$'));
772 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
773 while($row = array_shift($rows)) {
774 $q_meta[] = $row;
775 }
776 }
777 }
778 return $q_meta;
779 break;
780 case 'settings':
781 try {
782 if ($_SESSION['mailcow_cc_role'] == "admin") {
783 $settings['exclude_domains'] = json_decode($redis->Get('Q_EXCLUDE_DOMAINS'), true);
784 }
785 $settings['max_size'] = $redis->Get('Q_MAX_SIZE');
786 $settings['max_score'] = $redis->Get('Q_MAX_SCORE');
787 $settings['max_age'] = $redis->Get('Q_MAX_AGE');
788 $settings['retention_size'] = $redis->Get('Q_RETENTION_SIZE');
789 $settings['release_format'] = $redis->Get('Q_RELEASE_FORMAT');
790 $settings['subject'] = $redis->Get('Q_SUBJ');
791 $settings['sender'] = $redis->Get('Q_SENDER');
792 $settings['bcc'] = $redis->Get('Q_BCC');
793 $settings['redirect'] = $redis->Get('Q_REDIRECT');
794 $settings['html_tmpl'] = htmlspecialchars($redis->Get('Q_HTML'));
795 if (empty($settings['html_tmpl'])) {
796 $settings['html_tmpl'] = htmlspecialchars(file_get_contents("/tpls/quarantine.tpl"));
797 }
798 }
799 catch (RedisException $e) {
800 $_SESSION['return'][] = array(
801 'type' => 'danger',
802 'log' => array(__FUNCTION__, $_action, $_data_log),
803 'msg' => array('redis_error', $e)
804 );
805 return false;
806 }
807 return $settings;
808 break;
809 case 'details':
810 if (!is_numeric($_data) || empty($_data)) {
811 return false;
812 }
813 $stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `id`= :id');
814 $stmt->execute(array(':id' => $_data));
815 $row = $stmt->fetch(PDO::FETCH_ASSOC);
816 if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt']) || $_SESSION['mailcow_cc_role'] == 'admin') {
817 return $row;
818 }
819 logger(array('return' => array(
820 array(
821 'type' => 'danger',
822 'log' => array(__FUNCTION__, $_action, $_data_log),
823 'msg' => 'access_denied'
824 )
825 )));
826 return false;
827 break;
828 case 'hash_details':
829 $hash = trim($_data);
830 if (preg_match("/^([a-f0-9]{64})$/", $hash) === false) {
831 logger(array('return' => array(
832 array(
833 'type' => 'danger',
834 'log' => array(__FUNCTION__, $_action, $_data_log),
835 'msg' => 'access_denied'
836 )
837 )));
838 return false;
839 }
Matthias Andreas Benkard97f12022026-10-23 08:46:40 +0200840 $stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
Matthias Andreas Benkard4edeb9f2021-01-02 15:32:21 +0100841 $stmt->execute(array(':hash' => $hash));
842 return $stmt->fetch(PDO::FETCH_ASSOC);
843 break;
844 }
845}