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/MailerHelper.php +265 -93 10.2.2 → 11.1.0 View file →
@@ -12,14 +12,18 @@
12 12 use AcyMailing\Classes\UserClass;
13 13 use AcyMailerPhp\Exception;
14 14 use AcyMailerPhp\OAuth;
15 15 use AcyMailerPhp\Mailer;
16 -use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
16 +use AcyMailing\Libraries\Pelago\Emogrifier\CssInliner;
17 +use AcyMailing\Libraries\Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
17 18
18 19 class MailerHelper extends Mailer
19 20 {
20 21 const NEW_TRY_ERRORS = [1, 6];
22 + const REGEX_URL = '#<[^>]* href[ ]*=[ ]*"(?!mailto:|\#|ymsgr:|callto:|file:|ftp:|webcal:|skype:|tel:)([^"]+)"#Ui';
23 + const AUTOLOGIN_PARAMS = 'autoSubId=%7Bsubscriber:id%7D&amp;subKey=%7Bsubscriber:autologin_token%7Curlencode%7D';
21 24
25 + // Redefine PHPMailer protected attributes for dynamic texts system
22 26 public $to = [];
23 27 public $cc = [];
24 28 public $bcc = [];
25 29 public $ReplyTo = [];
@@ -46,10 +50,12 @@
46 50
47 51 public bool $report = true;
48 52 public bool $alreadyCheckedAddresses = false;
49 53 public int $errorNumber = 0;
54 + // Can be used when our sending method is temporary unavailable to not count it as a "real" failed
50 55 public bool $failedCounting = true;
51 56
57 + // Used by external code
52 58 public bool $autoAddUser = false;
53 59 public bool $userCreationTriggers = true;
54 60
55 61 public string $reportMessage = '';
@@ -54,20 +60,25 @@
54 60
55 61 public string $reportMessage = '';
56 62 public bool $dtextsFailed = false;
57 63
64 + // Should we track the sending of a message (used for welcoming message)
58 65 public bool $trackEmail = false;
59 66
67 + // Sending method used in the configuration
60 68 public string $externalMailer;
61 69
70 + // To import custom stylesheet from user
62 71 public string $stylesheet = '';
63 72 public array $settings;
64 73
74 + // Used to store special dynamic text content
65 75 public array $parameters = [];
66 76
67 77 private string $userLanguage = '';
68 78 private string $receiverEmail;
69 79
80 + // Special send statuses
70 81 private object $overrideEmailToSend;
71 82 public bool $isTest = false;
72 83 public bool $isSpamTest = false;
73 84 public bool $isForward = false;
@@ -77,10 +88,12 @@
77 88 public string $type;
78 89 public string $links_language;
79 90 public int $id = 0;
80 91 public ?object $mail = null;
92 + // Mail objects with replaceContent ran
81 93 private array $defaultMail = [];
82 94
95 + // Sending method list plugin
83 96 public array $listsIds = [];
84 97 private string $currentSendingMethod = '';
85 98 private array $currentMethodSetting = [];
86 99
@@ -87,13 +100,15 @@
87 100 public bool $isAbTest = false;
88 101 public bool $isTransactional = false;
89 102 public bool $isOneTimeMail = false;
90 103 public bool $isSendingMethodByListActive = false;
104 + public string $portError;
91 105
92 106 public function __construct()
93 107 {
94 108 parent::__construct();
95 109
110 + // The DKIM fails when the X-Mailer is added and the user uses their own keys, it makes no sense D:
96 111 $this->XMailer = ' ';
97 112 $this->SMTPAutoTLS = false;
98 113
99 114 $this->encodingHelper = new EncodingHelper();
@@ -114,11 +129,13 @@
114 129 }
115 130
116 131 $this->setSendingMethodSetting();
117 132
133 + //Do we have a DKIM validation?
118 134 if ($this->config->get('dkim', 0) && $this->Mailer != 'elasticemail') {
119 135 $this->DKIM_domain = $this->config->get('dkim_domain');
120 136 $this->DKIM_selector = $this->config->get('dkim_selector', 'acy');
137 + //Just in case of...
121 138 if (empty($this->DKIM_selector)) $this->DKIM_selector = 'acy';
122 139 $this->DKIM_passphrase = $this->config->get('dkim_passphrase');
123 140 $this->DKIM_identity = $this->config->get('dkim_identity');
124 141 $this->DKIM_private = trim($this->config->get('dkim_private'));
@@ -124,8 +141,9 @@
124 141 $this->DKIM_private = trim($this->config->get('dkim_private'));
125 142 $this->DKIM_private_string = trim($this->config->get('dkim_private'));
126 143 }
127 144
145 + //Set the Charset, by default 'utf-8'
128 146 $this->CharSet = strtolower($this->config->get('charset'));
129 147 if (empty($this->CharSet)) {
130 148 $this->CharSet = 'utf-8';
131 149 }
@@ -131,18 +149,18 @@
131 149 }
132 150
133 151 $this->clearAll();
134 152
153 + //Set the encoding format, should be 8 bit by default.
135 154 $this->Encoding = $this->config->get('encoding_format');
136 155 if (empty($this->Encoding)) {
137 156 $this->Encoding = '8bit';
138 157 }
139 158
159 + // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- This is for big regex, the default value is 100 000.
140 160 @ini_set('pcre.backtrack_limit', 1000000);
141 161
142 162 $this->SMTPOptions = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true]];
143 -
144 - $this->addParamInfo();
145 163 }
146 164
147 165 public function setSendingMethodSetting(): void
148 166 {
@@ -147,13 +165,14 @@
147 165 public function setSendingMethodSetting(): void
148 166 {
149 167 $externalSendingMethod = [];
150 168 acym_trigger('onAcymGetSendingMethods', [&$externalSendingMethod, true]);
151 - $externalSendingMethod = array_keys($externalSendingMethod['sendingMethods']);
169 + $externalSendingMethod = !empty($externalSendingMethod['sendingMethods']) ? array_keys($externalSendingMethod['sendingMethods']) : [];
152 170
153 171 $mailerMethodConfig = $this->getSendingMethod();
154 172 $this->currentSendingMethod = $mailerMethodConfig;
155 173
174 + // Default mailer is to use PHP's mail function
156 175 if ($mailerMethodConfig === 'google') {
157 176 $this->isSMTP();
158 177 $this->Host = 'smtp.gmail.com:465';
159 178 $this->SMTPAuth = true;
@@ -164,8 +183,9 @@
164 183 $this->setOAuth(
165 184 new OAuth(
166 185 [
167 186 'sendingMethod' => 'google',
187 + // https://developers.google.com/identity/protocols/oauth2/web-server#httprest_8
168 188 'tokenGenerationUrl' => 'https://oauth2.googleapis.com/token',
169 189 'userName' => $this->Username,
170 190 'clientId' => trim($this->getSendingMethodSettings('google_client_id')),
171 191 'clientSecret' => trim($this->getSendingMethodSettings('google_client_secret')),
@@ -192,8 +212,9 @@
192 212 $this->setOAuth(
193 213 new OAuth(
194 214 [
195 215 'sendingMethod' => 'outlook',
216 + // https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#refresh-the-access-token
196 217 'tokenGenerationUrl' => 'https://login.microsoftonline.com/'.$tenant.'/oauth2/v2.0/token',
197 218 'userName' => $this->Username,
198 219 'clientId' => trim($this->getSendingMethodSettings('outlook_client_id')),
199 220 'clientSecret' => trim($this->getSendingMethodSettings('outlook_client_secret')),
@@ -210,8 +231,9 @@
210 231 } elseif ($mailerMethodConfig === 'smtp') {
211 232 $this->isSMTP();
212 233 $this->Host = trim($this->getSendingMethodSettings('smtp_host'));
213 234 $port = $this->getSendingMethodSettings('smtp_port');
235 + // 465 is default port for SSL
214 236 if (empty($port) && $this->getSendingMethodSettings('smtp_secured') === 'ssl') {
215 237 $port = 465;
216 238 }
217 239 if (!empty($port)) {
@@ -265,8 +287,11 @@
265 287 $this->isMail();
266 288 }
267 289 }
268 290
291 + /**
292 + * Dynamically called from PHPMailer
293 + */
269 294 protected function externalSend(string $MIMEHeader, string $MIMEBody): bool
270 295 {
271 296 $fromName = empty($this->FromName) ? $this->config->get('from_name') : $this->FromName;
272 297 $reply_to = array_shift($this->ReplyTo);
@@ -326,8 +351,9 @@
326 351 return false;
327 352 }
328 353 }
329 354
355 + //Check if there is at least one reply to otherwise add the default one.
330 356 if (empty($this->ReplyTo) && empty($this->ReplyToQueue)) {
331 357 if (!empty($this->replyemail)) {
332 358 $replyToEmail = $this->replyemail;
333 359 } elseif ($this->config->get('from_as_replyto', 1) == 1) {
@@ -346,8 +372,9 @@
346 372
347 373 $this->acymAddReplyTo($replyToEmail, $replyToName);
348 374 }
349 375
376 + // Embed images if there are images to embed
350 377 $shouldEmbed = $this->config->get('embed_images', 0);
351 378 if (intval($shouldEmbed) === 1 && $this->Mailer !== 'elasticemail') {
352 379 $this->embedImages();
353 380 }
@@ -373,8 +400,9 @@
373 400
374 401 return false;
375 402 }
376 403
404 + //Check the from address
377 405 if (empty($this->From) || !acym_isValidEmail($this->From)) {
378 406 $this->reportMessage = acym_translation('ACYM_VALID_EMAIL').' ( '.acym_translation('ACYM_FROM_EMAIL').' : '.$this->From.' ) ';
379 407 $this->errorNumber = 9;
380 408 if ($this->report) {
@@ -398,10 +426,13 @@
398 426 if ($this->config->get('save_body') === '1') {
399 427 @file_put_contents(ACYM_ROOT.'acydebug_mail.html', $this->Body);
400 428 }
401 429
430 + //We will change the encoding format in case of its needed...
431 + //We always come from utf-8 to transform to something else!
402 432 $this->Body = htmlentities($this->Body);
403 433 $this->Body = htmlspecialchars_decode($this->Body);
434 + //Fix The Bat issues for special encoding as &sigmaf; was interpreted as &sigma;f;...
404 435 $this->Body = str_replace(['&amp;', '&sigmaf;'], ['&', 'ς'], $this->Body);
405 436
406 437 if ($this->CharSet !== 'utf-8') {
407 438 $this->Body = $this->encodingHelper->change($this->Body, 'UTF-8', $this->CharSet);
@@ -407,8 +438,9 @@
407 438 $this->Body = $this->encodingHelper->change($this->Body, 'UTF-8', $this->CharSet);
408 439 $this->Subject = $this->encodingHelper->change($this->Subject, 'UTF-8', $this->CharSet);
409 440 }
410 441
442 + // These characters can break the send process, let's remove them from the subject
411 443 $this->Subject = str_replace(
412 444 ['’', '“', '”', '–'],
413 445 ["'", '"', '"', '-'],
414 446 $this->Subject
@@ -413,8 +445,9 @@
413 445 ["'", '"', '"', '-'],
414 446 $this->Subject
415 447 );
416 448
449 + // BE CAREFUL! This space is not a space, it's a ALT0160 chr(194) by char(32) which means almost &nbsp;
417 450 $this->Body = str_replace(" ", ' ', $this->Body);
418 451
419 452 $externalSending = false;
420 453
@@ -445,8 +478,9 @@
445 478 }
446 479 }
447 480
448 481
482 + //display error if bloque is displayed... for free.fr especially
449 483 if (!empty($warnings) && strpos($warnings, 'bloque')) {
450 484 $result = false;
451 485 }
452 486
@@ -466,8 +500,9 @@
466 500 $this->reportMessage .= " \n\n ".$warnings;
467 501 }
468 502 $this->errorNumber = 1;
469 503 if ($this->report) {
504 + //We display the report here... we add a link to our doc for the "could not instantiate mail function".
470 505 $this->reportMessage = str_replace(
471 506 'Could not instantiate mail function',
472 507 '<a target="_blank" href="'.ACYM_DOCUMENTATION.'faq/could-not-instantiate-mail-function">'.acym_translation('ACYM_COUND_NOT_INSTANCIATE_MAIL_FUCNTION').'</a>',
473 508 $this->reportMessage
@@ -514,8 +549,9 @@
514 549 }
515 550
516 551 public function load(int $mailId, ?object $user = null): ?object
517 552 {
553 + // If it's already loaded return the email
518 554 if (isset($this->defaultMail[$mailId])) {
519 555 return $this->defaultMail[$mailId];
520 556 }
521 557
@@ -554,8 +590,9 @@
554 590 $acymLanguages['userLanguage'] = $this->userLanguage;
555 591 $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
556 592 }
557 593
594 + //We could not load the email...
558 595 if (empty($this->defaultMail[$mailId]->id)) {
559 596 unset($this->defaultMail[$mailId]);
560 597
561 598 return null;
@@ -563,14 +600,14 @@
563 600
564 601 if (!empty($this->defaultMail[$mailId]->attachments)) {
565 602 $this->defaultMail[$mailId]->attach = [];
566 603
567 - $attachments = json_decode($this->defaultMail[$mailId]->attachments);
604 + $attachments = json_decode($this->defaultMail[$mailId]->attachments, true);
568 605 foreach ($attachments as $oneAttach) {
569 606 $attach = new \stdClass();
570 - $attach->name = basename($oneAttach->filename);
571 - $attach->filename = str_replace(['/', '\\'], DS, ACYM_ROOT).$oneAttach->filename;
572 - $attach->url = ACYM_LIVE.$oneAttach->filename;
607 + $attach->name = basename($oneAttach['filename']);
608 + $attach->filename = str_replace(['/', '\\'], DS, ACYM_ROOT).$oneAttach['filename'];
609 + $attach->url = ACYM_LIVE.$oneAttach['filename'];
573 610 $this->defaultMail[$mailId]->attach[] = $attach;
574 611 }
575 612 }
576 613
@@ -576,8 +613,11 @@
576 613
577 614
578 615 $this->mailId = $mailId;
579 616 $this->id = $this->mailId;
617 + if (empty($this->defaultMail[$mailId]->language)) {
618 + $this->defaultMail[$mailId]->language = acym_getDefaultNewsletterLanguage();
619 + }
580 620 $this->mail = clone $this->defaultMail[$mailId];
581 621 acym_trigger('replaceContent', [&$this->defaultMail[$mailId], true]);
582 622
583 623 $this->prepareEmailContent($mailId);
@@ -587,8 +627,16 @@
587 627
588 628 return $this->defaultMail[$mailId];
589 629 }
590 630
631 + /**
632 + * @param int|string $mailId The mail Id
633 + * @param int|string|object $user Can be the user Id, an email address or the user object
634 + * @param array $options Additional options for bcc, attachments, etc...
635 + *
636 + * @return bool
637 + * @throws Exception
638 + */
591 639 public function sendOne($mailId, $user, array $options = []): bool
592 640 {
593 641 $this->clearAll();
594 642
@@ -597,9 +645,9 @@
597 645 $this->dtextsFailed = false;
598 646
599 647 if (empty($receiver->email)) {
600 648 $this->errorNumber = 4;
601 - $this->reportMessage = acym_translationSprintf('ACYM_SEND_ERROR_USER', '<b><i>'.acym_escape($user).'</i></b>');
649 + $this->reportMessage = acym_translationSprintf('ACYM_SEND_ERROR_USER', '<b><i>'.esc_html(is_object($user) ? $user->email : $user).'</i></b>');
602 650 if ($this->report) {
603 651 acym_enqueueMessage($this->reportMessage, 'error');
604 652 }
605 653
@@ -605,11 +653,13 @@
605 653
606 654 return false;
607 655 }
608 656
657 + // Can be a string for Joomla 3 and WordPress
609 658 $mailId = intval($mailId);
610 659 $receiver->id = intval($receiver->id);
611 660
661 + // Load the mail with global tags replaced
612 662 if (!$this->load($mailId, $receiver)) {
613 663 $this->reportMessage = 'Can not load the email with ID n°'.$mailId;
614 664 $this->errorNumber = 2;
615 665
@@ -626,14 +676,14 @@
626 676 $this->prepareBcc($mailId, $options);
627 677 $this->prepareAttachments($mailId, $options);
628 678 $this->replaceDynamicContent($mailId);
629 679 $this->prepareTracking($mailId, $receiver);
630 -
631 680 if (!$this->handleDtexts($receiver)) {
632 681 $this->dtextsFailed = true;
633 682
634 683 return false;
635 684 }
685 +
636 686 $status = $this->send();
637 687
638 688 $this->initStatistics($status, $receiver->id);
639 689
@@ -674,9 +724,9 @@
674 724
675 725 $followUpClass->triggerFollowUp(intval($followUp->id), intval($userId), $delay, $mailToSkip);
676 726 }
677 727
678 - public function statClick(int $mailId, int $userid, bool $fromStat = false): void
728 + public function statClick(int $mailId, int $userid, bool $fromStat = false, string $userkey = ''): void
679 729 {
680 730 if (!$fromStat && !in_array($this->type, MailClass::TYPES_WITH_STATS)) {
681 731 return;
682 732 }
@@ -697,9 +747,9 @@
697 747
698 748 $utmCampaign = acym_getAlias($mail->subject);
699 749 }
700 750
701 - preg_match_all('#<[^>]* href[ ]*=[ ]*"(?!mailto:|\#|ymsgr:|callto:|file:|ftp:|webcal:|skype:|tel:)([^"]+)"#Ui', $this->body, $results);
751 + preg_match_all(self::REGEX_URL, $this->body, $results);
702 752 if (empty($results)) {
703 753 return;
704 754 }
705 755
@@ -710,8 +760,9 @@
710 760 if ($countLinks[$url] === 1) {
711 761 continue;
712 762 }
713 763
764 + // Handle the Outlook buttons that double the link, we need to consider it as the same link that's on the main button element
714 765 $previousIsOutlook = false;
715 766 if (strpos($results[0][$key], '<v:roundrect') === 0) {
716 767 $previousLinkHandled = $results[0][$key];
717 768 if ($countLinks[$url] === 2) {
@@ -749,8 +800,9 @@
749 800
750 801 foreach ($results[1] as $i => $url) {
751 802 $urlsNotToTrack = [
752 803 'task=unsub',
804 + 'task=disableTracking',
753 805 'fonts.googleapis.com',
754 806 ];
755 807
756 808 $track = true;
@@ -761,22 +813,30 @@
761 813 break;
762 814 }
763 815 }
764 816
817 + //We don't track unsubscribe link
765 818 if (isset($urls[$results[0][$i]]) || !$track) {
766 819 continue;
767 820 }
768 821
822 + //We often need to check if the url is within the website... but we don't care about http or https
769 823 $simplifiedUrl = str_replace(['https://', 'http://', 'www.'], '', $url);
770 824 $simplifiedWebsite = str_replace(['https://', 'http://', 'www.'], '', ACYM_LIVE);
771 825 $internalUrl = strpos($simplifiedUrl, rtrim($simplifiedWebsite, '/')) === 0;
772 826
827 + // If this is an internal url
828 + //$subfolder : Record if the subfolder exists or not in which case it will be an external link
773 829 $subfolder = false;
774 830 if ($internalUrl) {
775 831 $urlWithoutBase = str_replace($simplifiedWebsite, '', $simplifiedUrl);
832 + // If there is a /, it means there could be a sub-folder
833 + //It can be separated with a ? as well like administrator?option=com_content
776 834 if (strpos($urlWithoutBase, '/') || strpos($urlWithoutBase, '?')) {
835 + // Get the supposed sub-folder name
777 836 $slashPosition = strpos($urlWithoutBase, '/');
778 837 $folderName = substr($urlWithoutBase, 0, !$slashPosition ? strpos($urlWithoutBase, '?') : $slashPosition);
838 + //There is no dot in a folder!
779 839 if (strpos($folderName, '.') === false) {
780 840 $subfolder = @is_dir(ACYM_ROOT.$folderName);
781 841 }
782 842 }
@@ -791,8 +851,9 @@
791 851 $args = [];
792 852 $args[] = empty($campaign->sending_params['utm_source']) ? 'utm_source=newsletter_'.$idToUse : 'utm_source='.urlencode($campaign->sending_params['utm_source']);
793 853 $args[] = empty($campaign->sending_params['utm_medium']) ? 'utm_medium=email' : 'utm_medium='.urlencode($campaign->sending_params['utm_medium']);
794 854 $args[] = empty($campaign->sending_params['utm_campaign']) ? 'utm_campaign='.$utmCampaign : 'utm_campaign='.urlencode($campaign->sending_params['utm_campaign']);
855 + //If we have an anchor we need to remove it and add it to the end of the url
795 856 $anchor = '';
796 857 if (strpos($url, '#') !== false) {
797 858 $anchor = substr($url, strpos($url, '#'));
798 859 $url = substr($url, 0, strpos($url, '#'));
@@ -802,40 +863,30 @@
802 863 $mytracker = $url.'&'.implode('&', $args);
803 864 } else {
804 865 $mytracker = $url.'?'.implode('&', $args);
805 866 }
867 + //We add back the anchor if we had one
806 868 $mytracker .= $anchor;
807 869 $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
808 870
871 + //Set the url variable so that we can use it later on...
809 872 $url = $mytracker;
810 873 }
811 874
812 875 if (strpos($trackingSystem, 'acymailing') !== false) {
813 - $isAutologin = false;
814 - $autologinParams = 'autoSubId=%7Bsubscriber:id%7D&amp;subKey=%7Bsubscriber:key%7Curlencode%7D';
815 - if (strpos($url, $autologinParams) !== false) {
816 - $isAutologin = true;
817 - $url = str_replace(
818 - [
819 - '?'.$autologinParams.'&amp;',
820 - '?'.$autologinParams,
821 - '&amp;'.$autologinParams,
822 - ],
823 - [
824 - '?',
825 - '',
826 - '',
827 - ],
828 - $url
829 - );
876 + $isAutologin = strpos($url, self::AUTOLOGIN_PARAMS) !== false;
877 + if ($isAutologin) {
878 + $url = $this->stripAutologinParams($url);
830 879 }
831 880
832 - if (preg_match('#passw|modify|\{|%7B#i', $url)) {
881 + if (preg_match('#passw|modify#i', $url)) {
833 882 continue;
834 883 }
835 884
885 + $urlForRegistration = $url;
886 +
836 887 if (!$fromStat) {
837 - $mytracker = $urlClass->getUrl($url, $mailId, $userid);
888 + $mytracker = $urlClass->getUrl($urlForRegistration, $mailId, $userid, $userkey);
838 889 }
839 890
840 891 if (empty($mytracker)) {
841 892 continue;
@@ -841,10 +892,10 @@
841 892 continue;
842 893 }
843 894
844 895 if ($isAutologin) {
845 - $mytracker .= strpos($mytracker, '?') === false ? '?' : '&amp;';
846 - $mytracker .= $autologinParams;
896 + $separator = strpos($mytracker, '?') === false ? '?' : '&amp;';
897 + $mytracker .= $separator.self::AUTOLOGIN_PARAMS;
847 898 }
848 899
849 900 $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
850 901 }
@@ -854,12 +905,17 @@
854 905 }
855 906
856 907 public function textVersion(string $html, bool $fullConvert = true): string
857 908 {
909 + //Replace relative links into absolute before replacing the text version so that we keep correct urls
858 910 $html = acym_absoluteURL($html);
859 911
912 + //If we come from a text version, we don't want to replace the spaces.
913 + //We will only do that if we come from an HTML Version to avoid breaking the user code
860 914 if ($fullConvert) {
915 + //As in HTML multiple spaces are interpreted as only one space, we remove the multiple spaces for the text version
861 916 $html = preg_replace('# +#', ' ', $html);
917 + //Same thing, return chars don't exist in html so we can simply remove them, neither \t
862 918 $html = str_replace(["\n", "\r", "\t"], '', $html);
863 919 }
864 920
865 921 $removepictureslinks = "#< *a[^>]*> *< *img[^>]*> *< *\/ *a *>#isU";
@@ -887,15 +943,20 @@
887 943 ['', '', '', '', "\n\n", "\n* ", "\n", "\n", '${2} ( ${1} )'],
888 944 $html
889 945 );
890 946
947 + //The striptags function may not do the job properly in some cases...
891 948 $text = preg_replace('#(&lt;|&\#60;)([^ \n\r\t])#i', '&lt; ${2}', $text);
892 949
893 - $text = str_replace([" ", "&nbsp;"], ' ', strip_tags($text));
950 + //BE CAREFUL!!!! This space is not a space, it's a ALT0160!! which means &nbsp;
951 + $text = str_replace([" ", "&nbsp;"], ' ', acym_stripTags($text));
952 + //BE CAREFUL!! That is magic code :) :) :)
894 953
954 + //@ is added on the call of html_entity_decode because on PHP 4, warnings are displayed using this function with utf-8 characters.
895 955 $text = trim(@html_entity_decode($text, ENT_QUOTES, 'UTF-8'));
896 956
897 957 if ($fullConvert) {
958 + //We do that one more time as some extra spaces may have appeared
898 959 $text = preg_replace('# +#', ' ', $text);
899 960 $text = preg_replace('#\n *\n\s+#', "\n\n", $text);
900 961 }
901 962
@@ -901,8 +962,11 @@
901 962
902 963 return $text;
903 964 }
904 965
966 + /**
967 + * @throws Exception
968 + */
905 969 protected function embedImages(): bool
906 970 {
907 971 preg_match_all('/(src|background)=[\'|"]([^"\']*)[\'|"]/Ui', $this->Body, $images);
908 972
@@ -925,12 +989,15 @@
925 989
926 990 $allImages = [];
927 991
928 992 foreach ($images[2] as $i => $url) {
993 + //We don't add twice the same images otherwise there is a bug
994 + //and the picture is really attached but not in hidden base64
929 995 if (isset($allImages[$url])) {
930 996 continue;
931 997 }
932 998
999 + // Don't embed images with a controller, most likely the stats picture
933 1000 if (strpos($url, 'ctrl=') !== false) {
934 1001 continue;
935 1002 }
936 1003
@@ -935,25 +1002,30 @@
935 1002 }
936 1003
937 1004 $allImages[$url] = 1;
938 1005
1006 + // We convert the url into local directory
939 1007 $path = acym_internalUrlToPath($url);
940 1008 $path = $this->removeAdditionalParams($path);
941 1009
1010 + // The file has to be an image located in the website's folder
1011 + $path = $this->getEmbeddableImagePath($path);
1012 + if (empty($path)) {
1013 + continue;
1014 + }
1015 +
1016 + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
1017 + if (!isset($mimetypes[$ext])) {
1018 + continue;
1019 + }
1020 +
942 1021 $filename = str_replace(['%', ' '], '_', basename($url));
943 1022 $filename = $this->removeAdditionalParams($filename);
944 1023
945 1024 $md5 = md5($filename);
946 1025 $cid = 'cid:'.$md5;
947 - $fileParts = explode(".", $filename);
948 - if (empty($fileParts[1])) {
949 - continue;
950 - }
951 - $ext = strtolower($fileParts[1]);
952 - if (!isset($mimetypes[$ext])) {
953 - continue;
954 - }
955 1026
1027 + // We only change the url if we were able to embed the image.
956 1028 if ($this->addEmbeddedImage($path, $md5, $filename, 'base64', $mimetypes[$ext])) {
957 1029 $this->Body = preg_replace('/'.preg_quote($images[0][$i], '/').'/Ui', $images[1][$i].'="'.$cid.'"', $this->Body);
958 1030 } else {
959 1031 $embedSuccess = false;
@@ -962,32 +1034,42 @@
962 1034
963 1035 return $embedSuccess;
964 1036 }
965 1037
966 - public function cleanText(?string $text): string
1038 + private function getEmbeddableImagePath(string $path): string
967 1039 {
968 - return trim(preg_replace('/(%0A|%0D|\n+|\r+)/i', '', (string)$text));
969 - }
1040 + // No url and no stream wrapper, we only read local files
1041 + if (empty($path) || preg_match('#^[a-z][a-z\d+.-]*://#i', $path) === 1) {
1042 + return '';
1043 + }
970 1044
971 - public function addParamInfo(): void
972 - {
973 - if (!empty($_SERVER)) {
974 - $serverinfo = [];
975 - foreach ($_SERVER as $oneKey => $oneInfo) {
976 - $serverinfo[] = $oneKey.' => '.strip_tags(print_r($oneInfo, true));
977 - }
978 - $this->addParam('serverinfo', implode('<br />', $serverinfo));
1045 + // The file must be inside the website's folder
1046 + $root = rtrim(ACYM_ROOT, DS).DS;
1047 + if (strpos($path, $root) !== 0 || preg_match('#(^|[/\\\\])\.\.([/\\\\]|$)#', substr($path, strlen($root))) === 1) {
1048 + return '';
979 1049 }
980 1050
981 - if (!empty($_REQUEST)) {
982 - $postinfo = [];
983 - foreach ($_REQUEST as $oneKey => $oneInfo) {
984 - $postinfo[] = $oneKey.' => '.strip_tags(print_r($oneInfo, true));
985 - }
986 - $this->addParam('postinfo', implode('<br />', $postinfo));
1051 + if (!is_file($path) || !is_readable($path)) {
1052 + return '';
987 1053 }
1054 +
1055 + // An image extension doesn't tell what the file actually contains, so we check the content itself
1056 + $imageInfo = @getimagesize($path);
1057 + if (empty($imageInfo['mime']) || strpos($imageInfo['mime'], 'image/') !== 0) {
1058 + return '';
1059 + }
1060 +
1061 + return $path;
988 1062 }
989 1063
1064 + public function cleanText(?string $text): string
1065 + {
1066 + return trim(preg_replace('/(%0A|%0D|\n+|\r+)/i', '', (string)$text));
1067 + }
1068 +
1069 + /**
1070 + * Adds shortcodes available in the email content
1071 + */
990 1072 public function addParam(string $name, $value): void
991 1073 {
992 1074 $tagName = '{'.$name.'}';
993 1075 $this->parameters[$tagName] = $value;
@@ -999,12 +1081,14 @@
999 1081
1000 1082 $overrideClass = new OverrideClass();
1001 1083 $override = $overrideClass->getMailByBaseContent($options['subject'], $options['message']);
1002 1084
1085 + // We let the CMS send the email
1003 1086 if (empty($override) && !$overrideAllEmails) {
1004 1087 return false;
1005 1088 }
1006 1089
1090 + // AcyMailing needs to send the email
1007 1091 $this->autoAddUser = true;
1008 1092
1009 1093 if (empty($override)) {
1010 1094 $this->trackEmail = false;
@@ -1016,8 +1100,33 @@
1016 1100 return $this->sendOverride($override, $options);
1017 1101 }
1018 1102 }
1019 1103
1104 + public function isPortOpen(int $port, string $targetServer): bool
1105 + {
1106 + if (!function_exists('fsockopen')) {
1107 + $this->portError = acym_translation('ACYM_FSOCKOPEN');
1108 +
1109 + return false;
1110 + }
1111 +
1112 + $errorCode = 0;
1113 + $errorMessage = 0;
1114 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fsockopen -- Connectivity test for SMTP ports.
1115 + $fp = @fsockopen($targetServer, $port, $errorCode, $errorMessage, 5);
1116 +
1117 + if (is_resource($fp)) {
1118 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing the socket resource opened above, not a filesystem handle.
1119 + fclose($fp);
1120 +
1121 + return true;
1122 + } else {
1123 + $this->portError = $errorCode.' - '.acym_utf8Encode($errorMessage);
1124 +
1125 + return false;
1126 + }
1127 + }
1128 +
1020 1129 private function getSendingMethodSettings(string $settingName, string $default = ''): string
1021 1130 {
1022 1131 if (empty($this->currentMethodSetting)) {
1023 1132 return $this->config->get($settingName, $default);
@@ -1112,36 +1221,32 @@
1112 1221
1113 1222 return true;
1114 1223 }
1115 1224
1225 + /**
1226 + * @param mixed $user
1227 + *
1228 + * @throws Exception
1229 + */
1116 1230 private function loadUser($user, bool $force = false): object
1117 1231 {
1118 - if (is_string($user) && strpos($user, '@')) {
1119 - $receiver = $this->userClass->getOneByEmail($user);
1120 -
1121 - if (empty($receiver)) {
1122 - if (($force || $this->autoAddUser) && acym_isValidEmail($user)) {
1123 - $newUser = new \stdClass();
1124 - $newUser->email = $user;
1125 - $this->userClass->checkVisitor = false;
1126 - $this->userClass->sendConf = false;
1127 - acym_setVar('acy_source', 'When sending a test');
1128 - $this->userClass->triggers = $this->userCreationTriggers;
1129 - $userId = $this->userClass->save($newUser);
1130 - $receiver = $this->userClass->getOneById($userId);
1131 - } else {
1132 - throw new Exception(acym_translation('ACYM_USER_NOT_FOUND'));
1133 - }
1134 - }
1232 + if (is_numeric($user)) {
1233 + $receiver = $this->userClass->getOneById($user);
1135 1234 } elseif (is_object($user)) {
1136 1235 $receiver = $user;
1137 - } else {
1138 - $receiver = $this->userClass->getOneById($user);
1236 + } elseif (is_string($user) && strpos($user, '@')) {
1237 + acym_setVar('acy_source', 'When sending a test');
1238 + $this->userClass->triggers = $this->userCreationTriggers;
1239 + $receiver = $this->userClass->getOneByEmail($user, $force || $this->autoAddUser);
1139 1240 }
1140 1241
1141 - $this->userLanguage = empty($receiver->language) ? acym_getLanguageTag() : $receiver->language;
1142 - $this->receiverEmail = $receiver->email;
1242 + if (empty($receiver)) {
1243 + throw new Exception(esc_attr(acym_translation('ACYM_USER_NOT_FOUND')));
1244 + }
1143 1245
1246 + $this->userLanguage = empty($receiver->language) ? acym_getDefaultNewsletterLanguage() : $receiver->language;
1247 + $this->receiverEmail = (string)($receiver->email ?? '');
1248 +
1144 1249 return $receiver;
1145 1250 }
1146 1251
1147 1252 private function prepareContent(int $mailId, array $options): void
@@ -1158,17 +1263,19 @@
1158 1263 if ($this->isTest && !empty($options['testNote'])) {
1159 1264 $this->Body = '<div style="text-align: center; padding: 25px; font-family: Poppins; font-size: 20px">'.$options['testNote'].'</div>'.$this->Body;
1160 1265 }
1161 1266
1267 + //We add the intro text at the top of the body
1162 1268 if (!empty($this->introtext)) {
1163 1269 $this->Body = $this->introtext.$this->Body;
1164 1270 }
1165 1271
1166 1272 if (!empty($this->defaultMail[$mailId]->preheader)) {
1167 - $preHeader = '<!--[if !mso 9]><!--><div style="visibility:hidden;mso-hide:all;font-size:0;color:transparent;height:0;line-height:0;max-height:0;max-width:0;opacity:0;overflow:hidden;">';
1168 - $preHeader .= $this->defaultMail[$mailId]->preheader.str_repeat('&nbsp;&zwnj;', 100);
1273 + $preHeader = '<!--[if !mso 9]><!--><div style="visibility:hidden;mso-hide:all;font-size:0;height:0;line-height:0;max-height:0;max-width:0;opacity:0;overflow:hidden;">';
1274 + $preHeader .= $this->defaultMail[$mailId]->preheader;
1169 1275 $preHeader .= '</div><!--<![endif]-->';
1170 1276
1277 + //We want to insert the preview at the start of the body so we match the start of the mail
1171 1278 preg_match('#(<(.*)<body(.*)>)#Uis', $this->Body, $matches);
1172 1279 if (empty($matches) || empty($matches[1])) {
1173 1280 $this->Body = $preHeader.$this->Body;
1174 1281 } else {
@@ -1184,9 +1291,10 @@
1184 1291 }
1185 1292
1186 1293 private function prepareHeaders(int $mailId, int $receiverId, array $options): void
1187 1294 {
1188 - $subject = base64_encode(rand(0, 9999999)).'AC'.$receiverId.'Y'.$this->defaultMail[$mailId]->id.'BA'.base64_encode(time().rand(0, 99999));
1295 + // Specify a messageID which will be kept by most mail clients and in the feedback loop
1296 + $subject = base64_encode(acym_rand(0, 9999999)).'AC'.$receiverId.'Y'.$this->defaultMail[$mailId]->id.'BA'.base64_encode(time().acym_rand(0, 99999));
1189 1297 $this->MessageID = '<'.preg_replace('|[^a-z0-9+_]|i', '', $subject).'@'.$this->serverHostname().'>';
1190 1298 $this->addCustomHeader('Feedback-ID', $this->defaultMail[$mailId]->id.':'.$receiverId.':'.$this->defaultMail[$mailId]->type.':'.base64_encode(ACYM_ROOT));
1191 1299
1192 1300 if (!empty($this->defaultMail[$mailId]->headers)) {
@@ -1204,8 +1312,9 @@
1204 1312 {
1205 1313 $receiverName = '';
1206 1314 if ($this->config->get('add_names', true)) {
1207 1315 $receiverName = $this->cleanText($receiver->name);
1316 + //We do not set a name if the name is the same as the email address, it prevents the email from being sent with some mail servers
1208 1317 if ($receiverName == $this->cleanText($receiver->email)) {
1209 1318 $receiverName = '';
1210 1319 }
1211 1320 }
@@ -1213,8 +1322,9 @@
1213 1322
1214 1323 $this->setFrom($this->getSendSettings('from_email', $mailId), $this->getSendSettings('from_name', $mailId));
1215 1324 $this->acymAddReplyTo($this->defaultMail[$mailId]->reply_to_email ?? '', $this->defaultMail[$mailId]->reply_to_name ?? '');
1216 1325
1326 + // This is the bounce email address
1217 1327 if (!empty($this->defaultMail[$mailId]->bounce_email)) {
1218 1328 $this->Sender = $this->cleanText($this->defaultMail[$mailId]->bounce_email);
1219 1329 } else {
1220 1330 $this->Sender = $this->cleanText($this->config->get('bounce_email'));
@@ -1220,8 +1330,11 @@
1220 1330 $this->Sender = $this->cleanText($this->config->get('bounce_email'));
1221 1331 }
1222 1332 }
1223 1333
1334 + /**
1335 + * @throws Exception
1336 + */
1224 1337 private function prepareBcc(int $mailId, array $options): void
1225 1338 {
1226 1339 $bccAddresses = [];
1227 1340
@@ -1235,12 +1348,20 @@
1235 1348 }
1236 1349
1237 1350 if (!empty($bccAddresses)) {
1238 1351 $bccAddresses = array_filter($bccAddresses);
1352 +
1353 + // loadUser() overwrites the current receiver, keep the real one
1354 + $receiverEmail = $this->receiverEmail;
1355 + $userLanguage = $this->userLanguage;
1356 +
1239 1357 foreach ($bccAddresses as $oneBccAddress) {
1240 1358 $this->loadUser($oneBccAddress, true);
1241 1359 $this->addBCC($oneBccAddress);
1242 1360 }
1361 +
1362 + $this->receiverEmail = $receiverEmail;
1363 + $this->userLanguage = $userLanguage;
1243 1364 }
1244 1365 }
1245 1366
1246 1367 private function prepareAttachments(int $mailId, array $options): void
@@ -1281,8 +1402,9 @@
1281 1402 private function replaceDynamicContent(int $mailId): void
1282 1403 {
1283 1404 $this->replaceParams();
1284 1405
1406 + // Left side are mail columns on which we replace the short codes
1285 1407 $this->body = &$this->Body;
1286 1408 $this->altbody = &$this->AltBody;
1287 1409 $this->subject = &$this->Subject;
1288 1410 $this->from = &$this->From;
@@ -1297,8 +1419,17 @@
1297 1419 $this->stylesheet = &$this->stylesheet;
1298 1420 $this->links_language = $this->defaultMail[$mailId]->links_language;
1299 1421 }
1300 1422
1423 + private function stripAutologinParams(string $url): string
1424 + {
1425 + return str_replace(
1426 + ['?'.self::AUTOLOGIN_PARAMS.'&amp;', '?'.self::AUTOLOGIN_PARAMS, '&amp;'.self::AUTOLOGIN_PARAMS],
1427 + ['?', '', ''],
1428 + $url
1429 + );
1430 + }
1431 +
1301 1432 private function prepareTracking(int $mailId, object $receiver): void
1302 1433 {
1303 1434 if ($this->canTrack($mailId, $receiver)) {
1304 1435 $receiver->id = intval($receiver->id);
@@ -1303,9 +1434,9 @@
1303 1434 if ($this->canTrack($mailId, $receiver)) {
1304 1435 $receiver->id = intval($receiver->id);
1305 1436 $this->statPicture($this->mailId, $receiver->id);
1306 1437 $this->body = acym_absoluteURL($this->body);
1307 - $this->statClick($this->mailId, $receiver->id);
1438 + $this->statClick($this->mailId, $receiver->id, false, $receiver->key ?? '');
1308 1439 if (acym_isTrackingSalesActive()) {
1309 1440 $this->trackingSales($this->mailId, $receiver->id);
1310 1441 }
1311 1442 }
@@ -1314,8 +1445,9 @@
1314 1445 private function handleDtexts(object $receiver): bool
1315 1446 {
1316 1447 $this->replaceParams();
1317 1448
1449 + // Sending a spam-test, use the current user instead
1318 1450 if (strpos($receiver->email, '@mt.acyba.com') !== false) {
1319 1451 $currentUser = $this->userClass->getOneByEmail(acym_currentUserEmail());
1320 1452 if (empty($currentUser)) {
1321 1453 $currentUser = $receiver;
@@ -1338,8 +1470,9 @@
1338 1470 if (!empty($acymLanguages['userLanguage'])) {
1339 1471 unset($acymLanguages['userLanguage']);
1340 1472 }
1341 1473
1474 + // A user based dtext may have inserted HTML content, we need to re-inline the CSS to it
1342 1475 foreach ($result as $oneResult) {
1343 1476 if (!empty($oneResult['emogrifier'])) {
1344 1477 $this->prepareEmailContent();
1345 1478 break;
@@ -1346,9 +1479,10 @@
1346 1479 }
1347 1480 }
1348 1481
1349 1482 if ($this->config->get('multiple_part', false)) {
1350 - $this->altbody = $this->textVersion($this->Body);
1483 + $bodyForText = preg_replace('#<head\b[^>]*>.*?</head\s*>#is', '', $this->Body);
1484 + $this->altbody = $this->textVersion($bodyForText);
1351 1485 }
1352 1486
1353 1487 $this->replaceParams();
1354 1488
@@ -1395,9 +1529,9 @@
1395 1529 private function statPicture(int $mailId, int $userId): void
1396 1530 {
1397 1531 $pictureLink = acym_frontendLink('frontstats&task=openStats&id='.$mailId.'&userid='.$userId, true, false);
1398 1532
1399 - $statPicture = '<img class="spict" alt="Statistics image" src="'.$pictureLink.'" border="0" width="50" height="1" />';
1533 + $statPicture = '<img class="spict" alt="" src="'.$pictureLink.'" border="0" width="50" height="1" />';
1400 1534
1401 1535 if (strpos($this->body, '</body>')) {
1402 1536 $this->body = str_replace('</body>', $statPicture.'</body>', $this->body);
1403 1537 } else {
@@ -1422,8 +1556,9 @@
1422 1556 }
1423 1557
1424 1558 $pluginHelper = new PluginHelper();
1425 1559
1560 + //We create an extra tag which contains all possible parameters...
1426 1561 $this->generateAllParams();
1427 1562
1428 1563 $vars = [
1429 1564 'Subject',
@@ -1448,12 +1583,16 @@
1448 1583 }
1449 1584 }
1450 1585 }
1451 1586
1587 + /**
1588 + * Create a new parameter called {alltags} which will include all the others
1589 + */
1452 1590 private function generateAllParams(): void
1453 1591 {
1454 1592 $result = '<table style="border:1px solid;border-collapse:collapse;" border="1" cellpadding="10"><tr><td>Tag</td><td>Value</td></tr>';
1455 1593 foreach ($this->parameters as $name => $value) {
1594 + //Just in case of...
1456 1595 if (!is_string($value)) continue;
1457 1596
1458 1597 $result .= '<tr><td>'.trim($name, '{}').'</td><td>'.$value.'</td></tr>';
1459 1598 }
@@ -1462,11 +1601,13 @@
1462 1601 }
1463 1602
1464 1603 private function sendOverride(object $override, array $options): bool
1465 1604 {
1466 - for ($i = 1 ; $i < count($override->parameters) ; $i++) {
1605 + // 2 - Prepare the email and params
1606 + for ($i = 1; $i < count($override->parameters); $i++) {
1467 1607 $oneParam = $override->parameters[$i];
1468 1608
1609 + // Joomla emails have links as text, convert them
1469 1610 $unmodified = $oneParam;
1470 1611 $oneParam = preg_replace(
1471 1612 '/(http|https):\/\/(.*)/',
1472 1613 '<a href="$1://$2" target="_blank">$1://$2</a>',
@@ -1481,11 +1622,13 @@
1481 1622 }
1482 1623
1483 1624 $this->addParam('subject', $options['subject']);
1484 1625
1626 + // 3 - Send the email
1485 1627 $this->overrideEmailToSend = $override;
1486 1628 $statusSend = $this->sendOne($override->id, $options['to'], $options);
1487 1629 if (!$statusSend && !empty($this->reportMessage)) {
1630 + // Something went wrong when trying to send the override, log the information in the cron logs file
1488 1631 $cronHelper = new CronHelper();
1489 1632 $cronHelper->saveReport([$this->reportMessage]);
1490 1633 }
1491 1634
@@ -1502,9 +1645,9 @@
1502 1645 if (!empty($mailId) && !empty($this->defaultMail[$mailId]->$mailType)) {
1503 1646 return $this->defaultMail[$mailId]->$mailType;
1504 1647 }
1505 1648
1506 - $lang = empty($this->userLanguage) ? acym_getLanguageTag() : $this->userLanguage;
1649 + $lang = empty($this->userLanguage) ? acym_getDefaultNewsletterLanguage() : $this->userLanguage;
1507 1650 $setting = $this->config->get($type);
1508 1651 $translation = $this->config->get('sender_info_translation');
1509 1652
1510 1653 if (!empty($translation)) {
@@ -1533,16 +1676,22 @@
1533 1676
1534 1677 private function inlineCSS(int $mailId): void
1535 1678 {
1536 1679 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1680 + if (empty($mail->body)) {
1681 + return;
1682 + }
1537 1683
1538 - global $emogrifiedMediaCSS;
1539 - $emogrifiedMediaCSS = '';
1684 + global $acymailingEmogrifiedMediaCSS;
1685 + $acymailingEmogrifiedMediaCSS = '';
1540 1686
1541 1687 $style = $this->getEmailStylesheet($mail);
1542 - $cssInliner = CssInlinerHelper::fromHtml($mail->body)->inlineCss(implode('', $style));
1688 + // Inline styles for mail client compatibility: https://www.caniemail.com/features/html-style/
1689 + // It also deletes the <style> tags from the body
1690 + $cssInliner = CssInliner::fromHtml($mail->body)->inlineCss(implode('', $style));
1543 1691 $domDocument = $cssInliner->getDomDocument();
1544 1692
1693 + // Remove the TinyMCE ids and the trailing ; in styles
1545 1694 $mail->body = preg_replace(
1546 1695 [
1547 1696 '# id="mce_\d+"#Ui',
1548 1697 '#(style="[^"]+);"#Ui',
@@ -1555,10 +1704,12 @@
1555 1704 ->removeRedundantClassesAfterCssInlined($cssInliner)
1556 1705 ->renderBodyContent()
1557 1706 );
1558 1707
1708 + // This character is added in the title and text blocks to prevent TinyMCE from adding an extra space when the block is empty
1559 1709 $mail->body = str_replace('&zwj;', '', $mail->body);
1560 1710
1711 + // Remove the extra spaces in the style attributes
1561 1712 $mail->body = preg_replace_callback(
1562 1713 '#style="([^"]+)"#Ui',
1563 1714 function ($matches) {
1564 1715 return 'style="'.str_replace(['; ', ': '], [';', ':'], $matches[1]).'"';
@@ -1577,10 +1728,13 @@
1577 1728 $finalContent .= '<meta http-equiv="Content-Type" content="text/html; charset='.strtolower($this->config->get('charset')).'" />'."\n";
1578 1729 $finalContent .= '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'."\n";
1579 1730 $finalContent .= '<title>'.$mail->subject.'</title>'."\n";
1580 1731
1581 - global $emogrifiedMediaCSS;
1582 - $finalContent .= '<style>'.$emogrifiedMediaCSS.'</style>';
1732 + //TODO maybe insert this CSS to a second <head> tag?
1733 + // Source: https://designmodo.com/html-css-emails/#:~:text=words%20of%20wisdom-,Internal%C2%A0CSS,-Undoubtedly%2C%20one%20of
1734 + // Apparently only for Yahoo on Android: https://www.caniemail.com/features/html-style/
1735 + global $acymailingEmogrifiedMediaCSS;
1736 + $finalContent .= '<style>'.$acymailingEmogrifiedMediaCSS.'</style>';
1583 1737 $finalContent .= '<!--[if mso]><style type="text/css">#acym__wysid__template center > table { width: 580px; }</style><![endif]-->';
1584 1738 $finalContent .= '<!--[if !mso]><!--><style>#acym__wysid__template center > table { width: 100%; }</style><!--<![endif]-->';
1585 1739
1586 1740 if (!empty($mail->headers)) {
@@ -1607,12 +1761,14 @@
1607 1761 private function getEmailStylesheet(object $mail): array
1608 1762 {
1609 1763 $style = [];
1610 1764
1765 + // If this is a drag and drop mail we add foundation css for email
1611 1766 if (strpos($mail->body, 'acym__wysid__template') !== false) {
1612 1767 static $foundationCSS = null;
1613 1768 if (empty($foundationCSS)) {
1614 1769 $foundationCSS = acym_fileGetContent(ACYM_MEDIA.'css'.DS.'libraries'.DS.'foundation_email.min.css');
1770 + // Remove the #acym__wysid__template prefix, not needed in sent emails
1615 1771 $foundationCSS = str_replace('#acym__wysid__template ', '', $foundationCSS);
1616 1772 }
1617 1773 $style['foundation'] = $foundationCSS;
1618 1774 }
@@ -1654,8 +1810,9 @@
1654 1810 }
1655 1811
1656 1812 $statusSend = $this->sendOne($this->overrideEmailToSend->id, $options['to'], $options);
1657 1813 if (!$statusSend && !empty($this->reportMessage)) {
1814 + // Something went wrong when trying to send the override, log the information in the cron logs file
1658 1815 $cronHelper = new CronHelper();
1659 1816 $cronHelper->addMessage($this->reportMessage);
1660 1817 $cronHelper->saveReport();
1661 1818 }
@@ -1662,9 +1819,17 @@
1662 1819
1663 1820 return $statusSend;
1664 1821 }
1665 1822
1823 + /* * * * * * * * * * * * * * * * *
1824 + * *
1825 + * Override PHPMailer's methods *
1826 + * *
1827 + * * * * * * * * * * * * * * * * */
1666 1828
1829 + /**
1830 + * @throws Exception
1831 + */
1667 1832 private function acymAddReplyTo(string $email, string $name): void
1668 1833 {
1669 1834 if (empty($email)) {
1670 1835 return;
@@ -1673,8 +1838,9 @@
1673 1838 $replyToName = $this->config->get('add_names', true) ? $this->cleanText(trim($name)) : '';
1674 1839 $replyToEmail = trim($email);
1675 1840
1676 1841 if (substr_count($replyToEmail, '@') > 1) {
1842 + //We have more than one reply to...
1677 1843 $replyToEmailArray = explode(';', str_replace([';', ','], ';', $replyToEmail));
1678 1844 $replyToNameArray = explode(';', str_replace([';', ','], ';', $replyToName));
1679 1845 foreach ($replyToEmailArray as $i => $oneReplyTo) {
1680 1846 $this->addReplyTo($this->cleanText($oneReplyTo), @$replyToNameArray[$i]);
@@ -1693,8 +1859,13 @@
1693 1859 $this->FromName = $this->cleanText($name);
1694 1860 }
1695 1861 }
1696 1862
1863 + /**
1864 + * Outputs debugging info via user-defined method
1865 + *
1866 + * @param string $str
1867 + */
1697 1868 protected function edebug($str)
1698 1869 {
1699 1870 if (strpos($this->ErrorInfo, $str) === false) {
1700 1871 $this->ErrorInfo .= ' '.$str;
@@ -1704,8 +1875,9 @@
1704 1875 public function getMailMIME()
1705 1876 {
1706 1877 $result = parent::getMailMIME();
1707 1878
1879 + //Added by Adrien on 11.02.2011 and then on 06 April 2011 otherwise we have 3 return char on phpMail or other functions
1708 1880 $result = rtrim($result, static::$LE);
1709 1881
1710 1882 if ($this->Mailer != 'mail') {
1711 1883 $result .= static::$LE.static::$LE;