| @@ -22,8 +22,9 @@ | ||
| 22 | 22 | const MESSAGE_TYPE_SUCCESS = 0; |
| 23 | 23 | const MESSAGE_TYPE_INFO = 1; |
| 24 | 24 | const MESSAGE_TYPE_ERROR = 2; |
| 25 | 25 | |
| 26 | + // Needed information for the connection | |
| 26 | 27 | private string $server; |
| 27 | 28 | private string $username; |
| 28 | 29 | private string $password; |
| 29 | 30 | private string $port; |
| @@ -32,8 +33,9 @@ | ||
| 32 | 33 | private bool $selfSigned; |
| 33 | 34 | private int $timeout; |
| 34 | 35 | private string $oAuthToken; |
| 35 | 36 | |
| 37 | + // Allowed extensions for uploaded files (attachments) | |
| 36 | 38 | private array $allowed_extensions = []; |
| 37 | 39 | protected int $nbMessages = 0; |
| 38 | 40 | public bool $report = false; |
| 39 | 41 | private MailerHelper $mailer; |
| @@ -46,11 +48,15 @@ | ||
| 46 | 48 | private bool $usePear = false; |
| 47 | 49 | private string $detectEmail; |
| 48 | 50 | private string $detectEmail2 = '/(([a-z0-9\-]+\.)+[a-z0-9]{2,8})\/([a-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+)*)/i'; |
| 49 | 51 | public array $messages = []; |
| 52 | + //Max execution time minus 4 seconds... we need to stop the process before this date. | |
| 50 | 53 | public int $stoptime = 0; |
| 54 | + //Check apache module "mod_security" to avoid flush issue | |
| 51 | 55 | protected bool $mod_security2 = false; |
| 56 | + //Number of ob_end_flush used in the process... | |
| 52 | 57 | public int $obend = 0; |
| 58 | + // Correspond to mailbox entity in database | |
| 53 | 59 | public object $action; |
| 54 | 60 | public array $attachments = []; |
| 55 | 61 | |
| 56 | 62 | private array $inlineImages = []; |
| @@ -168,8 +174,9 @@ | ||
| 168 | 174 | } |
| 169 | 175 | |
| 170 | 176 | $serverName = trim($this->server); |
| 171 | 177 | |
| 178 | + //We don't add back the ssl:// or tls:// if it's already there | |
| 172 | 179 | if (!empty($secure) && !strpos($serverName, '://')) { |
| 173 | 180 | $serverName = $secure.'://'.$serverName; |
| 174 | 181 | } |
| 175 | 182 | |
| @@ -192,9 +199,9 @@ | ||
| 192 | 199 | $login = $this->mailbox->login(trim($this->username), trim($this->password), 'USER'); |
| 193 | 200 | if (empty($login) || isset($login->code)) { |
| 194 | 201 | $warnings = ob_get_clean(); |
| 195 | 202 | if ($this->report) { |
| 196 | - acym_enqueueMessage(acym_translationSprintf('ACYM_ERROR_LOGIN', $this->username.':'.$this->password), 'error'); | |
| 203 | + acym_enqueueMessage(acym_translationSprintf('ACYM_ERROR_LOGIN', $this->username), 'error'); | |
| 197 | 204 | } |
| 198 | 205 | if (!empty($warnings) && $this->report) { |
| 199 | 206 | acym_display($warnings, 'warning'); |
| 200 | 207 | } |
| @@ -215,8 +222,9 @@ | ||
| 215 | 222 | return false; |
| 216 | 223 | } |
| 217 | 224 | |
| 218 | 225 | ob_start(); |
| 226 | + //First we reset the buffer or errors and warnings | |
| 219 | 227 | $buff = $this->callImapFunction('imap_alerts', []); |
| 220 | 228 | $buff = $this->callImapFunction('imap_errors', []); |
| 221 | 229 | |
| 222 | 230 | $timeout = intval($this->timeout); |
| @@ -243,12 +251,14 @@ | ||
| 243 | 251 | if ($this->server === 'outlook.office365.com') { |
| 244 | 252 | $serverName .= '/imap'; |
| 245 | 253 | } |
| 246 | 254 | |
| 255 | + //Add the secure protocol (TLS or SSL) | |
| 247 | 256 | if (!empty($secure)) { |
| 248 | 257 | $serverName .= '/'.$secure; |
| 249 | 258 | } |
| 250 | 259 | |
| 260 | + // Test if string contains '\' | |
| 251 | 261 | $email = trim($this->username); |
| 252 | 262 | if (strpos($email, '\\') !== false) { |
| 253 | 263 | [$user, $authuser] = explode('\\', $email); |
| 254 | 264 | [$x, $domain] = explode('@', $user); |
| @@ -258,8 +268,9 @@ | ||
| 258 | 268 | if ($this->selfSigned) { |
| 259 | 269 | $serverName .= '/novalidate-cert'; |
| 260 | 270 | } |
| 261 | 271 | |
| 272 | + //Add the method (imap by default) ex : pop3 | |
| 262 | 273 | if (!empty($protocol) && $this->server !== 'outlook.office365.com') { |
| 263 | 274 | $serverName .= '/service='.$protocol; |
| 264 | 275 | } |
| 265 | 276 | $serverName .= '}'; |
| @@ -364,9 +375,11 @@ | ||
| 364 | 375 | $inlineImages = []; |
| 365 | 376 | |
| 366 | 377 | $segments = explode('--'.$matches[1], $this->_message->html); |
| 367 | 378 | foreach ($segments as $segment) { |
| 379 | + // Find the segment containing the html and text version | |
| 368 | 380 | if (strpos($segment, 'Content-Type: text/plain') !== false) { |
| 381 | + // Check if there is another boundary | |
| 369 | 382 | $matches = []; |
| 370 | 383 | preg_match('#boundary="([^"]+)"#i', $segment, $matches); |
| 371 | 384 | |
| 372 | 385 | if (!empty($matches[1])) { |
| @@ -377,8 +390,9 @@ | ||
| 377 | 390 | |
| 378 | 391 | if (strpos($onePart, 'Content-Transfer-Encoding') !== false) { |
| 379 | 392 | preg_match('#Content-Transfer-Encoding: (.+)#i', $onePart, $encoding); |
| 380 | 393 | $encoding = trim($encoding[1]); |
| 394 | + // Someone has an issue when the $encoding is equal to 'Content-Type:' only | |
| 381 | 395 | if (!empty($encoding) && $encoding !== 'Content-Type:') { |
| 382 | 396 | $content = mb_convert_encoding($content, 'UTF-8', $encoding); |
| 383 | 397 | } |
| 384 | 398 | } |
| @@ -392,9 +406,9 @@ | ||
| 392 | 406 | } |
| 393 | 407 | } else { |
| 394 | 408 | $this->_message->html .= ' '.trim(preg_replace('#(charset=".*?\r\n)|Content-(Type|ID|Disposition|Transfer-Encoding):.*?\r\n#is', "", $segment)); |
| 395 | 409 | } |
| 396 | - } elseif (preg_match("#Content-Type: .*?/(png|jpg|jpeg|gif)#i", $segment) !== false) { | |
| 410 | + } elseif (preg_match("#Content-Type: .*?/(png|jpg|jpeg|gif)#i", $segment) === 1) { | |
| 397 | 411 | preg_match('#name="([^"]+)"#i', $segment, $filename); |
| 398 | 412 | if (empty($filename) || empty($filename[1])) { |
| 399 | 413 | continue; |
| 400 | 414 | } |
| @@ -407,13 +421,20 @@ | ||
| 407 | 421 | } |
| 408 | 422 | |
| 409 | 423 | $extension = substr($filename, $extensionPos + 1); |
| 410 | 424 | |
| 425 | + if (!in_array(strtolower($extension), $this->allowed_extensions) || acym_isForbiddenFileExtension($filename)) { | |
| 426 | + continue; | |
| 427 | + } | |
| 428 | + | |
| 429 | + // Make sure there is no double extension or space | |
| 411 | 430 | $filename = preg_replace('#[^a-zA-Z0-9]#Uis', '_', substr($filename, 0, $extensionPos)); |
| 412 | 431 | |
| 432 | + // Get the upload folder and create it if needed | |
| 413 | 433 | $uploadFolder = str_replace(['/', '\\'], DS, acym_getFilesFolder()); |
| 414 | 434 | $pathToUpload = ACYM_ROOT.trim($uploadFolder, DS).DS; |
| 415 | 435 | |
| 436 | + // Rename if needed | |
| 416 | 437 | if (file_exists($pathToUpload.$filename.'.'.$extension)) { |
| 417 | 438 | $fileNumber = 1; |
| 418 | 439 | while (file_exists($pathToUpload.$filename.'_('.$fileNumber.').'.$extension)) { |
| 419 | 440 | $fileNumber++; |
| @@ -420,9 +441,9 @@ | ||
| 420 | 441 | } |
| 421 | 442 | $filename = $filename.'_('.$fileNumber.')'; |
| 422 | 443 | } |
| 423 | 444 | |
| 424 | - if ($attachments && preg_match("#Content-Disposition: attachment;#i", $segment) !== false) { | |
| 445 | + if ($attachments && preg_match("#Content-Disposition: attachment;#i", $segment) === 1) { | |
| 425 | 446 | $data = trim(substr($segment, strpos($segment, "\r\n\r\n"))); |
| 426 | 447 | if (strpos($segment, 'Content-Transfer-Encoding: base64') !== false) { |
| 427 | 448 | $data = base64_decode($data); |
| 428 | 449 | } |
| @@ -478,9 +499,9 @@ | ||
| 478 | 499 | } |
| 479 | 500 | if (!empty($this->_message->header->sender_email) && preg_match($this->detectEmail, $this->_message->header->sender_email, $results)) { |
| 480 | 501 | $this->_message->header->sender_email = $results[0]; |
| 481 | 502 | } |
| 482 | - $this->_message->header->sender_name = strip_tags(@$this->_message->headerinfo['from'] ?? ''); | |
| 503 | + $this->_message->header->sender_name = acym_stripTags(@$this->_message->headerinfo['from'] ?? ''); | |
| 483 | 504 | $this->_message->header->reply_to_email = $this->_message->header->sender_email; |
| 484 | 505 | $this->_message->header->reply_to_name = $this->_message->header->sender_name; |
| 485 | 506 | $this->_message->header->from_email = $this->_message->header->sender_email; |
| 486 | 507 | $this->_message->header->from_name = $this->_message->header->sender_name; |
| @@ -500,8 +521,9 @@ | ||
| 500 | 521 | |
| 501 | 522 | $this->_message->html = ''; |
| 502 | 523 | $this->_message->text = ''; |
| 503 | 524 | |
| 525 | + //Multipart message : type == 1 | |
| 504 | 526 | if ($this->_message->structure->type == 1) { |
| 505 | 527 | $this->_message->contentType = 2; |
| 506 | 528 | if ($this->_message->structure->subtype === 'MIXED') { |
| 507 | 529 | $allParts = $this->explodeBodyMixed($this->_message->structure); |
| @@ -521,8 +543,9 @@ | ||
| 521 | 543 | } |
| 522 | 544 | $this->_message->text .= $decodedContent."\n\n- - -\n\n"; |
| 523 | 545 | } |
| 524 | 546 | |
| 547 | + // Only for mailbox actions | |
| 525 | 548 | if (!empty($this->action) && $attachments) { |
| 526 | 549 | if ((isset($onePart->parameters) && is_array($onePart->parameters)) || (isset($onePart->dparameters) && is_array($onePart->dparameters))) { |
| 527 | 550 | $this->uploadAttachment($onePart, $num); |
| 528 | 551 | } elseif (isset($onePart->parts) && is_array($onePart->parts)) { |
| @@ -539,17 +562,20 @@ | ||
| 539 | 562 | if (!empty($this->inlineImages)) { |
| 540 | 563 | $this->_message->html = str_replace(array_keys($this->inlineImages), $this->inlineImages, $this->_message->html); |
| 541 | 564 | } |
| 542 | 565 | } else { |
| 566 | + $body = $this->callImapFunction('imap_body', [$this->mailbox, $this->_message->messageNB]); | |
| 567 | + $decodedBody = empty($body) ? '' : $this->decodeContent($body, $this->_message->structure); | |
| 543 | 568 | if ($this->_message->structure->subtype === 'HTML') { |
| 544 | 569 | $this->_message->contentType = 1; |
| 545 | - $this->_message->html = $this->decodeContent($this->callImapFunction('imap_body', [$this->mailbox, $this->_message->messageNB]), $this->_message->structure); | |
| 570 | + $this->_message->html = $decodedBody; | |
| 546 | 571 | } else { |
| 547 | 572 | $this->_message->contentType = 0; |
| 548 | - $this->_message->text = $this->decodeContent($this->callImapFunction('imap_body', [$this->mailbox, $this->_message->messageNB]), $this->_message->structure); | |
| 573 | + $this->_message->text = $decodedBody; | |
| 549 | 574 | } |
| 550 | 575 | } |
| 551 | 576 | |
| 577 | + //Decode the subject | |
| 552 | 578 | if (!empty($this->_message->subject)) { |
| 553 | 579 | $this->_message->subject = $this->decodeHeader($this->_message->subject); |
| 554 | 580 | } else { |
| 555 | 581 | $this->_message->subject = ''; |
| @@ -564,8 +590,9 @@ | ||
| 564 | 590 | } |
| 565 | 591 | |
| 566 | 592 | private function uploadAttachment($attachment, $num): void |
| 567 | 593 | { |
| 594 | + // It may be a real attachment, or an inline image | |
| 568 | 595 | if (!isset($attachment->disposition) || (strtolower($attachment->disposition) !== 'attachment' && strtolower($attachment->disposition) !== 'inline')) { |
| 569 | 596 | return; |
| 570 | 597 | } |
| 571 | 598 | |
| @@ -582,22 +609,27 @@ | ||
| 582 | 609 | } |
| 583 | 610 | } |
| 584 | 611 | } |
| 585 | 612 | |
| 613 | + // No extension => no upload | |
| 586 | 614 | if ($extensionPos === false) { |
| 587 | 615 | return; |
| 588 | 616 | } |
| 589 | 617 | |
| 618 | + // Upload only allowed extensions in the configuration, and never executable ones | |
| 590 | 619 | $extension = substr($filename, $extensionPos + 1); |
| 591 | - if (!in_array(strtolower($extension), $this->allowed_extensions)) { | |
| 620 | + if (!in_array(strtolower($extension), $this->allowed_extensions) || acym_isForbiddenFileExtension($filename)) { | |
| 592 | 621 | return; |
| 593 | 622 | } |
| 594 | 623 | |
| 624 | + // Make sure there is no double extension or space | |
| 595 | 625 | $filename = preg_replace('#[^a-zA-Z0-9]#Uis', '_', substr($filename, 0, $extensionPos)); |
| 596 | 626 | |
| 627 | + // Get the upload folder and create it if needed | |
| 597 | 628 | $uploadFolder = str_replace(['/', '\\'], DS, acym_getFilesFolder()); |
| 598 | 629 | $pathToUpload = ACYM_ROOT.trim($uploadFolder, DS).DS; |
| 599 | 630 | |
| 631 | + // Rename if needed | |
| 600 | 632 | if (file_exists($pathToUpload.$filename.'.'.$extension)) { |
| 601 | 633 | $fileNumber = 1; |
| 602 | 634 | while (file_exists($pathToUpload.$filename.'_('.$fileNumber.').'.$extension)) { |
| 603 | 635 | $fileNumber++; |
| @@ -604,10 +636,17 @@ | ||
| 604 | 636 | } |
| 605 | 637 | $filename = $filename.'_('.$fileNumber.')'; |
| 606 | 638 | } |
| 607 | 639 | |
| 608 | - $data = $this->decodeContent($this->callImapFunction('imap_fetchbody', [$this->mailbox, $this->_message->messageNB, $num]), $attachment); | |
| 640 | + $content = $this->callImapFunction('imap_fetchbody', [$this->mailbox, $this->_message->messageNB, $num]); | |
| 609 | 641 | |
| 642 | + // We could not fetch the part, there is nothing to upload | |
| 643 | + if (empty($content)) { | |
| 644 | + return; | |
| 645 | + } | |
| 646 | + | |
| 647 | + $data = $this->decodeContent($content, $attachment); | |
| 648 | + | |
| 610 | 649 | try { |
| 611 | 650 | if (acym_writeFile($pathToUpload.$filename.'.'.$extension, $data)) { |
| 612 | 651 | if ( |
| 613 | 652 | strtolower($attachment->disposition) !== 'attachment' |
| @@ -630,8 +669,9 @@ | ||
| 630 | 669 | |
| 631 | 670 | public function handleMessages(): void |
| 632 | 671 | { |
| 633 | 672 | $maxMessages = min($this->nbMessages, $this->config->get('bounce_max', 0)); |
| 673 | + //500 messages maximum at once | |
| 634 | 674 | if (empty($maxMessages)) { |
| 635 | 675 | $maxMessages = min($this->nbMessages, 500); |
| 636 | 676 | } |
| 637 | 677 | |
| @@ -640,9 +680,12 @@ | ||
| 640 | 680 | $modules = apache_get_modules(); |
| 641 | 681 | $this->mod_security2 = in_array('mod_security2', $modules); |
| 642 | 682 | } |
| 643 | 683 | |
| 684 | + /*This is to avoid the blank page... and it apparently works! ;) */ | |
| 685 | + // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Disabled to prevent blank output pages. | |
| 644 | 686 | @ini_set('output_buffering', 'off'); |
| 687 | + // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Disabled to prevent blank output pages. | |
| 645 | 688 | @ini_set('zlib.output_compression', 0); |
| 646 | 689 | |
| 647 | 690 | |
| 648 | 691 | if (!headers_sent()) { |
| @@ -650,17 +693,19 @@ | ||
| 650 | 693 | @ob_end_flush(); |
| 651 | 694 | } |
| 652 | 695 | } |
| 653 | 696 | |
| 654 | - $disp = "<div style='position:fixed; top:3px;left:3px;background-color : white;border : 1px solid grey; padding : 3px;font-size:14px'>"; | |
| 655 | - $disp .= acym_translation('ACYM_BOUNCE_HANDLING'); | |
| 656 | - $disp .= ': <span id="counter">0</span> / '.$maxMessages; | |
| 657 | - $disp .= '</div>'; | |
| 658 | - $disp .= '<script type="text/javascript" language="javascript">'; | |
| 659 | - $disp .= 'var mycounter = document.getElementById("counter");'; | |
| 660 | - $disp .= 'function setCounter(val){ mycounter.innerHTML=val;}'; | |
| 661 | - $disp .= '</script>'; | |
| 662 | - echo $disp; | |
| 697 | + ?> | |
| 698 | + <div style="position:fixed; top:3px;left:3px;background-color : white;border : 1px solid grey; padding : 3px;font-size:14px"> | |
| 699 | + <?php echo esc_html(acym_translation('ACYM_BOUNCE_HANDLING')); ?> | |
| 700 | + : <span id="counter">0</span> / <?php echo intval($maxMessages); ?> | |
| 701 | + </div> | |
| 702 | + <script type="text/javascript" language="javascript"> | |
| 703 | + function setCounter(val) { | |
| 704 | + document.getElementById('counter').innerHTML = val; | |
| 705 | + } | |
| 706 | + </script> | |
| 707 | + <?php | |
| 663 | 708 | if (function_exists('ob_flush')) { |
| 664 | 709 | @ob_flush(); |
| 665 | 710 | } |
| 666 | 711 | if (!$this->mod_security2) { |
| @@ -667,8 +712,9 @@ | ||
| 667 | 712 | @flush(); |
| 668 | 713 | } |
| 669 | 714 | } |
| 670 | 715 | |
| 716 | + //We load all the published rules | |
| 671 | 717 | $rules = $this->ruleClass->getAll(); |
| 672 | 718 | $rules = array_filter($rules, function ($rule) { |
| 673 | 719 | return $rule->active; |
| 674 | 720 | }); |
| @@ -676,8 +722,9 @@ | ||
| 676 | 722 | $msgNB = $maxMessages; |
| 677 | 723 | $listClass = new ListClass(); |
| 678 | 724 | $this->allLists = $listClass->getAll('id'); |
| 679 | 725 | |
| 726 | + //Exclude some email addresses... | |
| 680 | 727 | $replyemail = $this->config->get('reply_email'); |
| 681 | 728 | $fromemail = $this->config->get('from_email'); |
| 682 | 729 | $bouncemail = $this->config->get('bounce_email'); |
| 683 | 730 | $removeEmails = '#('.str_replace(['%'], ['@'], $this->config->get('bounce_username')); |
| @@ -693,9 +740,9 @@ | ||
| 693 | 740 | $removeEmails .= ')#i'; |
| 694 | 741 | |
| 695 | 742 | while (($msgNB > 0) && ($this->_message = $this->getMessage($msgNB))) { |
| 696 | 743 | if ($this->report) { |
| 697 | - echo '<script type="text/javascript" language="javascript">setCounter('.($maxMessages - $msgNB + 1).')</script>'; | |
| 744 | + echo '<script type="text/javascript" language="javascript">setCounter('.esc_html($maxMessages - $msgNB + 1).')</script>'; | |
| 698 | 745 | if (function_exists('ob_flush')) { |
| 699 | 746 | @ob_flush(); |
| 700 | 747 | } |
| 701 | 748 | if (!$this->mod_security2) { |
| @@ -704,8 +751,9 @@ | ||
| 704 | 751 | } |
| 705 | 752 | $this->_message->messageNB = $msgNB; |
| 706 | 753 | $msgNB--; |
| 707 | 754 | |
| 755 | + //We could not retrieve the message... we continue with the next message | |
| 708 | 756 | if (!$this->decodeMessage()) { |
| 709 | 757 | $this->display(acym_translation('ACYM_ERROR_RETRIEVING_MESSAGE'), self::MESSAGE_TYPE_ERROR, $maxMessages - $this->_message->messageNB + 1); |
| 710 | 758 | continue; |
| 711 | 759 | } |
| @@ -714,17 +762,19 @@ | ||
| 714 | 762 | $this->_message->subject = ''; |
| 715 | 763 | } |
| 716 | 764 | |
| 717 | 765 | $this->_message->analyseText = $this->_message->html.' '.$this->_message->text; |
| 766 | + //We add the from in the list of possible e-mail address to check | |
| 718 | 767 | if (!empty($this->_message->header->from_email)) { |
| 719 | 768 | $this->_message->analyseText .= ' '.$this->_message->header->from_email; |
| 720 | 769 | } |
| 721 | 770 | $this->display( |
| 722 | - '<b>'.acym_translation('ACYM_EMAIL_SUBJECT').' : '.strip_tags($this->_message->subject).'</b>', | |
| 771 | + '<b>'.acym_translation('ACYM_EMAIL_SUBJECT').' : '.acym_stripTags($this->_message->subject).'</b>', | |
| 723 | 772 | self::MESSAGE_TYPE_SUCCESS, |
| 724 | 773 | $maxMessages - $this->_message->messageNB + 1 |
| 725 | 774 | ); |
| 726 | 775 | |
| 776 | + //Identify the user and the e-mail... | |
| 727 | 777 | preg_match('#AC([0-9]+)Y([0-9]+)BA#i', $this->_message->analyseText, $resultsVars); |
| 728 | 778 | if (!empty($resultsVars[1])) { |
| 729 | 779 | $this->_message->userid = $resultsVars[1]; |
| 730 | 780 | } |
| @@ -732,10 +782,12 @@ | ||
| 732 | 782 | $this->_message->mailid = $resultsVars[2]; |
| 733 | 783 | } |
| 734 | 784 | |
| 735 | 785 | if (empty($this->_message->userid)) { |
| 786 | + //We will need the e-mail itself in that case... :p | |
| 736 | 787 | preg_match_all($this->detectEmail, $this->_message->analyseText, $results); |
| 737 | 788 | |
| 789 | + //Still no result? We try to find others | |
| 738 | 790 | if (empty($results[0])) { |
| 739 | 791 | preg_match_all($this->detectEmail2, $this->_message->analyseText, $results2); |
| 740 | 792 | for ($i = 0; $i < count($results2[0]); $i++) { |
| 741 | 793 | $results[0][] = $results2[3][$i].'@'.$results2[1][$i]; |
| @@ -744,10 +796,13 @@ | ||
| 744 | 796 | |
| 745 | 797 | if (!empty($results[0])) { |
| 746 | 798 | $alreadyChecked = []; |
| 747 | 799 | foreach ($results[0] as $oneEmail) { |
| 800 | + //We will find the e-mail if it's not in the list of incorrect e-mail addresses | |
| 748 | 801 | if (!preg_match($removeEmails, $oneEmail)) { |
| 802 | + //We will keep this one, so we make sure it's strtolower | |
| 749 | 803 | $this->_message->subemail = strtolower($oneEmail); |
| 804 | + //We already checked this e-mail address... no need to try it a second time | |
| 750 | 805 | if (!empty($alreadyChecked[$this->_message->subemail])) { |
| 751 | 806 | continue; |
| 752 | 807 | } |
| 753 | 808 | |
| @@ -769,8 +824,9 @@ | ||
| 769 | 824 | } |
| 770 | 825 | } |
| 771 | 826 | |
| 772 | 827 | if (empty($this->_message->mailid) && !empty($this->_message->userid)) { |
| 828 | + //We can check if we have a user and only one e-mail sent for this user, it's obviously the e-mail we just sent!! | |
| 773 | 829 | $this->_message->mailid = acym_loadResult( |
| 774 | 830 | 'SELECT `mail_id` FROM #__acym_user_stat WHERE `user_id` = '.intval($this->_message->userid).' ORDER BY `send_date` DESC' |
| 775 | 831 | ); |
| 776 | 832 | } |
| @@ -775,8 +831,9 @@ | ||
| 775 | 831 | ); |
| 776 | 832 | } |
| 777 | 833 | |
| 778 | 834 | foreach ($rules as $oneRule) { |
| 835 | + //We stop as soon as we find a good rule... | |
| 779 | 836 | if ($this->handleRule($oneRule)) { |
| 780 | 837 | break; |
| 781 | 838 | } |
| 782 | 839 | } |
| @@ -784,8 +841,9 @@ | ||
| 784 | 841 | if ($msgNB % 50 == 0) { |
| 785 | 842 | $this->userActions(); |
| 786 | 843 | } |
| 787 | 844 | |
| 845 | + //We don't have time to finish the process? Ok, we stop it now! | |
| 788 | 846 | if (!empty($this->stoptime) && time() > $this->stoptime) { |
| 789 | 847 | break; |
| 790 | 848 | } |
| 791 | 849 | } |
| @@ -793,8 +851,12 @@ | ||
| 793 | 851 | $this->userActions(); |
| 794 | 852 | $this->close(); |
| 795 | 853 | } |
| 796 | 854 | |
| 855 | + /** | |
| 856 | + * Execute actions on the subscriber... and record the statistics | |
| 857 | + * We group them to not have performance issues | |
| 858 | + */ | |
| 797 | 859 | private function userActions(): void |
| 798 | 860 | { |
| 799 | 861 | if (!empty($this->deletedUsers)) { |
| 800 | 862 | acym_arrayToInteger($this->deletedUsers); |
| @@ -804,8 +866,9 @@ | ||
| 804 | 866 | |
| 805 | 867 | if (!empty($this->blockedUsers)) { |
| 806 | 868 | acym_arrayToInteger($this->blockedUsers); |
| 807 | 869 | $this->userClass->deactivate($this->blockedUsers); |
| 870 | + //We delete any other e-mail from the queue as well | |
| 808 | 871 | $allUsersId = implode(',', $this->blockedUsers); |
| 809 | 872 | acym_query('DELETE FROM `#__acym_queue` WHERE `user_id` IN ('.$allUsersId.')'); |
| 810 | 873 | $this->blockedUsers = []; |
| 811 | 874 | } |
| @@ -811,8 +874,11 @@ | ||
| 811 | 874 | } |
| 812 | 875 | |
| 813 | 876 | if (!empty($this->bounceMessages)) { |
| 814 | 877 | foreach ($this->bounceMessages as $mailid => $bouncedata) { |
| 878 | + //Do we have some bounce details to update? | |
| 879 | + //If so, we will load the current bouncedetails and update it properly. | |
| 880 | + //bouncedetails is an array of bounceRule => nbTimes used | |
| 815 | 881 | |
| 816 | 882 | $updateBounceDetails = ''; |
| 817 | 883 | if (!empty($bouncedata['bouncedetails'])) { |
| 818 | 884 | $bouncedetails = acym_loadResult('SELECT `bounce_details` FROM #__acym_mail_stat WHERE mail_id = '.intval($mailid)); |
| @@ -842,8 +908,9 @@ | ||
| 842 | 908 | ',', |
| 843 | 909 | $valueInsert |
| 844 | 910 | ).' ON DUPLICATE KEY UPDATE `bounce` = `bounce` + 1, bounce_rule=VALUES(bounce_rule)' |
| 845 | 911 | ); |
| 912 | + //We updated some profiles... let's make sure we really handle only unique bounces then and don't count it twice | |
| 846 | 913 | acym_arrayToInteger($bouncedata['userids']); |
| 847 | 914 | $realUniqueBounces = acym_loadResult( |
| 848 | 915 | 'SELECT COUNT(*) |
| 849 | 916 | FROM #__acym_user_stat |
| @@ -870,8 +937,9 @@ | ||
| 870 | 937 | if (empty($regex)) { |
| 871 | 938 | return false; |
| 872 | 939 | } |
| 873 | 940 | |
| 941 | + //Do it based on the config of the rule... | |
| 874 | 942 | |
| 875 | 943 | $analyseText = ''; |
| 876 | 944 | if (in_array('senderInfo', $oneRule->executed_on)) { |
| 877 | 945 | $analyseText .= ' '; |
| @@ -895,8 +963,9 @@ | ||
| 895 | 963 | $analyseText .= ' '.$this->_message->text; |
| 896 | 964 | } |
| 897 | 965 | } |
| 898 | 966 | |
| 967 | + //Because it's easier to handle it that way... for multilines. | |
| 899 | 968 | $analyseText = str_replace(["\n", "\r", "\t"], ' ', $analyseText); |
| 900 | 969 | |
| 901 | 970 | if (!preg_match('#'.$regex.'#ims', $analyseText)) { |
| 902 | 971 | return false; |
| @@ -930,8 +999,9 @@ | ||
| 930 | 999 | |
| 931 | 1000 | return $message; |
| 932 | 1001 | } |
| 933 | 1002 | |
| 1003 | + //To display nice error messages... | |
| 934 | 1004 | if ( |
| 935 | 1005 | in_array('delete_user_subscription', $oneRule->action_user) |
| 936 | 1006 | || in_array('unsubscribe_user', $oneRule->action_user) |
| 937 | 1007 | || in_array('subscribe_user', $oneRule->action_user) |
| @@ -973,8 +1043,9 @@ | ||
| 973 | 1043 | $this->_message->subemail = $this->_message->userid; |
| 974 | 1044 | } |
| 975 | 1045 | |
| 976 | 1046 | |
| 1047 | + //handle this rule in the stats | |
| 977 | 1048 | if (!empty($this->_message->mailid)) { |
| 978 | 1049 | $mail = $this->mailClass->getOneById($this->_message->mailid); |
| 979 | 1050 | } else { |
| 980 | 1051 | $mail = null; |
| @@ -980,8 +1051,9 @@ | ||
| 980 | 1051 | $mail = null; |
| 981 | 1052 | } |
| 982 | 1053 | |
| 983 | 1054 | if ($oneRule->increment_stats && !empty($this->_message->mailid) && !empty($mail)) { |
| 1055 | + //Init the stats... | |
| 984 | 1056 | if (empty($this->bounceMessages[$this->_message->mailid])) { |
| 985 | 1057 | $this->bounceMessages[$this->_message->mailid] = []; |
| 986 | 1058 | $this->bounceMessages[$this->_message->mailid]['nbbounces'] = 0; |
| 987 | 1059 | $this->bounceMessages[$this->_message->mailid]['userids'] = []; |
| @@ -988,11 +1060,14 @@ | ||
| 988 | 1060 | $this->bounceMessages[$this->_message->mailid]['bouncedetails'] = []; |
| 989 | 1061 | $this->bounceMessages[$this->_message->mailid]['ruletriggered'] = []; |
| 990 | 1062 | } |
| 991 | 1063 | |
| 1064 | + //Increment the global stats... | |
| 992 | 1065 | $this->bounceMessages[$this->_message->mailid]['nbbounces']++; |
| 993 | 1066 | |
| 1067 | + //Increment the detailed stats... | |
| 994 | 1068 | $ruleName = $oneRule->name.' [ID '.$oneRule->id.'] '; |
| 1069 | + //We add a @ just in case the rule was not already defined... | |
| 995 | 1070 | if (empty($this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName])) { |
| 996 | 1071 | $this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName] = 1; |
| 997 | 1072 | } else { |
| 998 | 1073 | $this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName]++; |
| @@ -999,8 +1074,9 @@ | ||
| 999 | 1074 | } |
| 1000 | 1075 | |
| 1001 | 1076 | $user = $this->userClass->getOneById($this->_message->userid); |
| 1002 | 1077 | if (!empty($this->_message->userid) && !in_array('delete_user', $oneRule->action_user) && !empty($user)) { |
| 1078 | + //Increment the bounce number in the user stat table but only if we don't delete the subscriber | |
| 1003 | 1079 | $this->bounceMessages[$this->_message->mailid]['userids'][] = intval($this->_message->userid); |
| 1004 | 1080 | $this->bounceMessages[$this->_message->mailid]['ruletriggered'][intval($this->_message->userid)] = $oneRule->name.' ['.acym_translation( |
| 1005 | 1081 | 'ACYM_ID' |
| 1006 | 1082 | ).' '.$oneRule->id.']'; |
| @@ -1006,9 +1082,11 @@ | ||
| 1006 | 1082 | ).' '.$oneRule->id.']'; |
| 1007 | 1083 | } |
| 1008 | 1084 | } |
| 1009 | 1085 | |
| 1086 | + //Make sure we have enough messages to really execute this | |
| 1010 | 1087 | if (!empty($oneRule->execute_action_after) && $oneRule->execute_action_after > 1) { |
| 1088 | + //Let's load the number of bounces the user has and then exit or not... | |
| 1011 | 1089 | if (empty($this->_message->mailid)) { |
| 1012 | 1090 | $this->_message->mailid = 0; |
| 1013 | 1091 | } |
| 1014 | 1092 | $nb = intval( |
| @@ -1028,8 +1106,9 @@ | ||
| 1028 | 1106 | return $message; |
| 1029 | 1107 | } |
| 1030 | 1108 | } |
| 1031 | 1109 | |
| 1110 | + //If we delete the subscriber, it's the last action we execute | |
| 1032 | 1111 | if (in_array('delete_user', $oneRule->action_user)) { |
| 1033 | 1112 | $message .= ' | '.acym_translationSprintf('ACYM_USER_X_DELETED', $this->_message->subemail); |
| 1034 | 1113 | $this->deletedUsers[] = intval($this->_message->userid); |
| 1035 | 1114 | |
| @@ -1035,8 +1114,9 @@ | ||
| 1035 | 1114 | |
| 1036 | 1115 | return $message; |
| 1037 | 1116 | } |
| 1038 | 1117 | |
| 1118 | + //We will need a default listid after... | |
| 1039 | 1119 | $listId = 0; |
| 1040 | 1120 | if (in_array('subscribe_user', $oneRule->action_user) && !empty($oneRule->action_user['subscribe_user_list'])) { |
| 1041 | 1121 | $listId = $oneRule->action_user['subscribe_user_list']; |
| 1042 | 1122 | |
| @@ -1111,28 +1191,35 @@ | ||
| 1111 | 1191 | private function actionMessage(object &$oneRule): string |
| 1112 | 1192 | { |
| 1113 | 1193 | $message = ''; |
| 1114 | 1194 | |
| 1195 | + //Fix the rule if needed... when the forwarded user is the same as the bounce e-mail address... | |
| 1115 | 1196 | if (!empty($oneRule->action_message['forward_to'])) { |
| 1116 | 1197 | if ( |
| 1117 | 1198 | strtolower($oneRule->action_message['forward_to']) === strtolower($this->config->get('bounce_username')) |
| 1118 | 1199 | || strtolower($oneRule->action_message['forward_to']) === strtolower($this->config->get('bounce_email')) |
| 1119 | 1200 | ) { |
| 1201 | + //We don't forward it | |
| 1120 | 1202 | $oneRule->action_message['forward_to'] = ''; |
| 1203 | + //We don't delete it | |
| 1121 | 1204 | unset($oneRule->action_message['delete_message']); |
| 1122 | 1205 | $message .= ' | '.acym_translation('ACYM_BOUNCE_NOT_FORWARD'); |
| 1123 | 1206 | } |
| 1124 | 1207 | } |
| 1125 | 1208 | |
| 1209 | + //Handle actions on the message itself | |
| 1126 | 1210 | |
| 1211 | + // We only delete the message if the forward didn't fail and the save in history succeeded | |
| 1127 | 1212 | $donotdelete = false; |
| 1128 | 1213 | |
| 1129 | 1214 | if (in_array('save_message', $oneRule->action_message) && !empty($this->_message->userid) && !in_array('delete_user', $oneRule->action_user)) { |
| 1215 | + //We have a userid, should we save the message in the database? | |
| 1130 | 1216 | $data = []; |
| 1131 | 1217 | $data[] = 'SUBJECT::'.@htmlentities($this->_message->subject, ENT_COMPAT, 'UTF-8'); |
| 1132 | 1218 | $data[] = 'ACY_RULE::'.$oneRule->id.' '.$oneRule->name; |
| 1133 | 1219 | $data[] = 'REPLYTO_ADDRESS::'.$this->_message->header->reply_to_name.' ( '.$this->_message->header->reply_to_email.' )'; |
| 1134 | 1220 | $data[] = 'FROM_ADDRESS::'.$this->_message->header->from_name.' ( '.$this->_message->header->from_email.' )'; |
| 1221 | + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Logging information. | |
| 1135 | 1222 | $data[] = @htmlentities(print_r($this->_message->headerinfo, true), ENT_COMPAT, 'UTF-8'); |
| 1136 | 1223 | if (empty($this->_message->mailid)) { |
| 1137 | 1224 | $this->_message->mailid = 0; |
| 1138 | 1225 | } |
| @@ -1151,9 +1238,11 @@ | ||
| 1151 | 1238 | } |
| 1152 | 1239 | } |
| 1153 | 1240 | |
| 1154 | 1241 | |
| 1242 | + //We don't forward the message if it's the same mailbox! | |
| 1155 | 1243 | if (!empty($oneRule->action_message['forward_to'])) { |
| 1244 | + //Get the forward address : | |
| 1156 | 1245 | $this->mailer->clearAll(); |
| 1157 | 1246 | $this->mailer->Subject = 'BOUNCE FORWARD : '.$this->_message->subject; |
| 1158 | 1247 | |
| 1159 | 1248 | if (substr_count($oneRule->action_message['forward_to'], '@') > 1) { |
| @@ -1165,8 +1254,9 @@ | ||
| 1165 | 1254 | foreach ($forwardAddresses as $oneForwardAddress) { |
| 1166 | 1255 | $this->mailer->AddAddress($this->mailer->cleanText($oneForwardAddress)); |
| 1167 | 1256 | } |
| 1168 | 1257 | |
| 1258 | + //Add the rule at the top so we know why the message has been forwarded. | |
| 1169 | 1259 | $info = acym_translation('ACYM_BOUNCE_RULE').' ['.acym_translation('ACYM_ID').' '.$oneRule->id.'] '.acym_translation($oneRule->name); |
| 1170 | 1260 | if (!empty($this->_message->html)) { |
| 1171 | 1261 | $this->mailer->isHTML(); |
| 1172 | 1262 | if (!empty($this->_message->userid)) { |
| @@ -1183,8 +1273,11 @@ | ||
| 1183 | 1273 | $this->mailer->isHTML(false); |
| 1184 | 1274 | $this->mailer->Body = $info."\n".$this->_message->text; |
| 1185 | 1275 | } |
| 1186 | 1276 | |
| 1277 | + //We add all other extra information just in case of we could use them... | |
| 1278 | + //original-rcpt-to ? http://tools.ietf.org/html/rfc5965 | |
| 1279 | + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Logging information. | |
| 1187 | 1280 | $this->mailer->Body .= print_r($this->_message->headerinfo, true); |
| 1188 | 1281 | $replyAddress = trim(@$this->_message->header->reply_to_email, '<> '); |
| 1189 | 1282 | if (!empty($replyAddress)) { |
| 1190 | 1283 | if ($this->config->get('mailer_method') === \plgAcymAcymailer::SENDING_METHOD_ID) { |
| @@ -1234,8 +1327,11 @@ | ||
| 1234 | 1327 | |
| 1235 | 1328 | $this->_message->header->$email = $var[0]->mailbox.'@'.@$var[0]->host; |
| 1236 | 1329 | } |
| 1237 | 1330 | |
| 1331 | + /** | |
| 1332 | + * If num is empty then it's a message otherwise it's a send status | |
| 1333 | + */ | |
| 1238 | 1334 | protected function display(string $message, int $type = self::MESSAGE_TYPE_SUCCESS, string $num = ''): void |
| 1239 | 1335 | { |
| 1240 | 1336 | $this->messages[] = $message; |
| 1241 | 1337 | |
| @@ -1249,14 +1345,14 @@ | ||
| 1249 | 1345 | $color = 'red'; |
| 1250 | 1346 | } |
| 1251 | 1347 | |
| 1252 | 1348 | if (!empty($num)) { |
| 1253 | - echo '<br />'.$num.' : '; | |
| 1349 | + echo '<br />'.esc_html($num).' : '; | |
| 1254 | 1350 | } else { |
| 1255 | 1351 | echo '<br /> '; |
| 1256 | 1352 | } |
| 1257 | 1353 | |
| 1258 | - echo '<span style="color: '.$color.'">'.$message.'</span>'; | |
| 1354 | + echo '<span style="color: '.esc_attr($color).'">'.esc_html($message).'</span>'; | |
| 1259 | 1355 | |
| 1260 | 1356 | if (function_exists('ob_flush')) { |
| 1261 | 1357 | @ob_flush(); |
| 1262 | 1358 | } |
| @@ -1266,11 +1362,13 @@ | ||
| 1266 | 1362 | } |
| 1267 | 1363 | |
| 1268 | 1364 | public function decodeHeader(string $input): string |
| 1269 | 1365 | { |
| 1366 | + // Remove white space between encoded-words | |
| 1270 | 1367 | $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); |
| 1271 | 1368 | $currentCharset = false; |
| 1272 | 1369 | |
| 1370 | + // For each encoded-word... | |
| 1273 | 1371 | while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) { |
| 1274 | 1372 | $encoded = $matches[1]; |
| 1275 | 1373 | $charset = $matches[2]; |
| 1276 | 1374 | $encoding = $matches[3]; |
| @@ -1292,8 +1390,9 @@ | ||
| 1292 | 1390 | $currentCharset = $charset; |
| 1293 | 1391 | $input = str_replace($encoded, $text, $input); |
| 1294 | 1392 | } |
| 1295 | 1393 | |
| 1394 | + //If we have a charset and we can handle it... | |
| 1296 | 1395 | if (!empty($currentCharset) && in_array($currentCharset, $this->allCharsets)) { |
| 1297 | 1396 | $input = $this->encodingHelper->change($input, $currentCharset, 'UTF-8'); |
| 1298 | 1397 | } |
| 1299 | 1398 | |
| @@ -1328,16 +1427,22 @@ | ||
| 1328 | 1427 | if (empty($struct->parts)) { |
| 1329 | 1428 | return $allParts; |
| 1330 | 1429 | } |
| 1331 | 1430 | |
| 1431 | + //counts real content | |
| 1332 | 1432 | $c = 0; |
| 1333 | 1433 | foreach ($struct->parts as $part) { |
| 1334 | 1434 | if ($part->type == 1) { |
| 1435 | + //There are more parts....: | |
| 1335 | 1436 | if ($part->subtype === 'MIXED') { |
| 1437 | + // refreshing current path | |
| 1336 | 1438 | $path = $this->incPath($path); |
| 1439 | + //create a new path-id (ex.:2.0) | |
| 1337 | 1440 | $newpath = $path.'.0'; |
| 1441 | + //fetch new parts | |
| 1338 | 1442 | $allParts = array_merge($this->explodeBody($part, $newpath), $allParts); |
| 1339 | 1443 | } else { |
| 1444 | + //Alternativ / rfc / signed | |
| 1340 | 1445 | $newpath = $this->incPath($path); |
| 1341 | 1446 | $path = $this->incPath($path); |
| 1342 | 1447 | $allParts = array_merge($this->explodeBody($part, $newpath, true), $allParts); |
| 1343 | 1448 | } |
| @@ -1342,12 +1447,15 @@ | ||
| 1342 | 1447 | $allParts = array_merge($this->explodeBody($part, $newpath, true), $allParts); |
| 1343 | 1448 | } |
| 1344 | 1449 | } else { |
| 1345 | 1450 | $c++; |
| 1451 | + //creating new tree if this is part of a alternativ or rfc message: | |
| 1346 | 1452 | if ($c == 1 && $inline) { |
| 1347 | 1453 | $path = $path.'.0'; |
| 1348 | 1454 | } |
| 1455 | + //saving content: | |
| 1349 | 1456 | $path = $this->incPath($path); |
| 1457 | + //print "<br> Content ".$path."<br>"; //debug information | |
| 1350 | 1458 | $allParts[$path] = $part; |
| 1351 | 1459 | } |
| 1352 | 1460 | } |
| 1353 | 1461 | |
| @@ -1353,8 +1461,9 @@ | ||
| 1353 | 1461 | |
| 1354 | 1462 | return $allParts; |
| 1355 | 1463 | } |
| 1356 | 1464 | |
| 1465 | + //Increases the Path to the parts: | |
| 1357 | 1466 | private function incPath(string $path): string |
| 1358 | 1467 | { |
| 1359 | 1468 | $newPath = ''; |
| 1360 | 1469 | $path_elements = explode('.', $path); |
| @@ -1359,11 +1468,14 @@ | ||
| 1359 | 1468 | $newPath = ''; |
| 1360 | 1469 | $path_elements = explode('.', $path); |
| 1361 | 1470 | $limit = count($path_elements); |
| 1362 | 1471 | for ($i = 0; $i < $limit; $i++) { |
| 1472 | + //last element | |
| 1363 | 1473 | if ($i == $limit - 1) { |
| 1474 | + // new Part-Number | |
| 1364 | 1475 | $newPath .= $path_elements[$i] + 1; |
| 1365 | 1476 | } else { |
| 1477 | + //rebuild "1.2.2"-Chronology | |
| 1366 | 1478 | $newPath .= $path_elements[$i].'.'; |
| 1367 | 1479 | } |
| 1368 | 1480 | } |
| 1369 | 1481 | |
| @@ -1373,8 +1485,9 @@ | ||
| 1373 | 1485 | private function decodeContent(string $content, object $structure): string |
| 1374 | 1486 | { |
| 1375 | 1487 | $encoding = $structure->encoding; |
| 1376 | 1488 | |
| 1489 | + //First we decode the content properly | |
| 1377 | 1490 | if ($encoding == 2) { |
| 1378 | 1491 | $content = imap_binary($content); |
| 1379 | 1492 | } elseif ($encoding == 3) { |
| 1380 | 1493 | $content = base64_decode($content, true); |
| @@ -1381,8 +1494,9 @@ | ||
| 1381 | 1494 | } elseif ($encoding == 4) { |
| 1382 | 1495 | $content = imap_qprint($content); |
| 1383 | 1496 | } |
| 1384 | 1497 | |
| 1498 | + // Now we convert into utf-8! only for distribution lists | |
| 1385 | 1499 | if (!empty($this->action)) { |
| 1386 | 1500 | $charset = $this->getMailParam($structure); |
| 1387 | 1501 | if (!empty($charset) && strtoupper($charset) !== 'UTF-8') { |
| 1388 | 1502 | $content = $this->encodingHelper->change($content, $charset, 'UTF-8'); |
| @@ -1390,8 +1504,9 @@ | ||
| 1390 | 1504 | |
| 1391 | 1505 | return $content; |
| 1392 | 1506 | } |
| 1393 | 1507 | |
| 1508 | + //It can't be more than 100 000 characters, it's already plenty enough for bounce handling... we avoid embedded pictures or big attachments we could not catch before. | |
| 1394 | 1509 | return substr($content, 0, 100000); |
| 1395 | 1510 | } |
| 1396 | 1511 | |
| 1397 | 1512 | private function getMailParam(object $params): string |