PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.0
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.0
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
← All changes | back/Helpers/BounceHelper.php +136 -21 10.11.1 → 11.1.0 View file →
@@ -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 />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
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