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 +332 -132 10.1.4 → 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();
@@ -103,9 +118,9 @@
103 118 $this->mailArchiveClass = new MailArchiveClass();
104 119 $this->config = acym_config();
105 120
106 121 $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
107 - $this->Sender = $this->cleanText($this->config->get('bounce_email'));
122 + $this->Sender = $this->cleanText($this->getSendSettings('bounce_email'));
108 123 if (empty($this->Sender)) {
109 124 $this->Sender = '';
110 125 }
111 126 $maxLineLength = $this->config->get('mailer_wordwrap', 0);
@@ -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,17 +165,75 @@
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
156 - if ($mailerMethodConfig === 'smtp') {
174 + // Default mailer is to use PHP's mail function
175 + if ($mailerMethodConfig === 'google') {
157 176 $this->isSMTP();
177 + $this->Host = 'smtp.gmail.com:465';
178 + $this->SMTPAuth = true;
179 + $this->Username = trim($this->getSendingMethodSettings('google_username'));
180 + $this->AuthType = 'XOAUTH2';
181 + $this->SMTPSecure = 'ssl';
182 +
183 + $this->setOAuth(
184 + new OAuth(
185 + [
186 + 'sendingMethod' => 'google',
187 + // https://developers.google.com/identity/protocols/oauth2/web-server#httprest_8
188 + 'tokenGenerationUrl' => 'https://oauth2.googleapis.com/token',
189 + 'userName' => $this->Username,
190 + 'clientId' => trim($this->getSendingMethodSettings('google_client_id')),
191 + 'clientSecret' => trim($this->getSendingMethodSettings('google_client_secret')),
192 + 'oauthToken' => trim($this->getSendingMethodSettings('google_access_token')),
193 + 'oauthTokenExpiration' => trim($this->getSendingMethodSettings('google_access_token_expiration')),
194 + 'refreshToken' => trim($this->getSendingMethodSettings('google_refresh_token')),
195 + ]
196 + )
197 + );
198 +
199 + if (empty($this->Sender)) {
200 + $this->Sender = strpos($this->Username, '@') ? $this->Username : $this->getSendingMethodSettings('from_email');
201 + }
202 + } elseif ($mailerMethodConfig === 'outlook') {
203 + $this->isSMTP();
204 + $tenant = trim($this->getSendingMethodSettings('outlook_tenant', 'consumers'));
205 + $host = $tenant === 'consumers' ? 'smtp-mail.outlook.com' : 'smtp.office365.com';
206 + $this->Host = $host.':587';
207 + $this->SMTPAuth = true;
208 + $this->Username = trim($this->getSendingMethodSettings('outlook_username'));
209 + $this->AuthType = 'XOAUTH2';
210 + $this->SMTPSecure = 'tls';
211 +
212 + $this->setOAuth(
213 + new OAuth(
214 + [
215 + 'sendingMethod' => 'outlook',
216 + // https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#refresh-the-access-token
217 + 'tokenGenerationUrl' => 'https://login.microsoftonline.com/'.$tenant.'/oauth2/v2.0/token',
218 + 'userName' => $this->Username,
219 + 'clientId' => trim($this->getSendingMethodSettings('outlook_client_id')),
220 + 'clientSecret' => trim($this->getSendingMethodSettings('outlook_client_secret')),
221 + 'oauthToken' => trim($this->getSendingMethodSettings('outlook_access_token')),
222 + 'oauthTokenExpiration' => trim($this->getSendingMethodSettings('outlook_access_token_expiration')),
223 + 'refreshToken' => trim($this->getSendingMethodSettings('outlook_refresh_token')),
224 + ]
225 + )
226 + );
227 +
228 + if (empty($this->Sender)) {
229 + $this->Sender = strpos($this->Username, '@') ? $this->Username : $this->getSendingMethodSettings('from_email');
230 + }
231 + } elseif ($mailerMethodConfig === 'smtp') {
232 + $this->isSMTP();
158 233 $this->Host = trim($this->getSendingMethodSettings('smtp_host'));
159 234 $port = $this->getSendingMethodSettings('smtp_port');
235 + // 465 is default port for SSL
160 236 if (empty($port) && $this->getSendingMethodSettings('smtp_secured') === 'ssl') {
161 237 $port = 465;
162 238 }
163 239 if (!empty($port)) {
@@ -164,43 +240,18 @@
164 240 $this->Host .= ':'.$port;
165 241 }
166 242 $this->SMTPAuth = (bool)$this->getSendingMethodSettings('smtp_auth', '1');
167 243 $this->Username = trim($this->getSendingMethodSettings('smtp_username'));
168 - $connectionType = $this->getSendingMethodSettings('smtp_type');
169 - $hostName = explode(':', $this->Host)[0];
170 244
171 - if (OAuth::hostRequireOauth($hostName, $connectionType)) {
172 - $this->AuthType = 'XOAUTH2';
173 -
174 - $clientSecret = trim($this->getSendingMethodSettings('smtp_secret'));
175 - $clientId = trim($this->getSendingMethodSettings('smtp_clientId'));
176 - $refreshToken = trim($this->getSendingMethodSettings('smtp_refresh_token'));
177 - $oauthToken = trim($this->getSendingMethodSettings('smtp_token'));
178 - $expiredIn = trim($this->getSendingMethodSettings('smtp_token_expireIn'));
179 -
180 - $oauth = new OAuth(
181 - [
182 - 'userName' => $this->Username,
183 - 'clientSecret' => $clientSecret,
184 - 'oauthToken' => $oauthToken,
185 - 'clientId' => $clientId,
186 - 'refreshToken' => $refreshToken,
187 - 'expiredIn' => $expiredIn,
188 - 'host' => $hostName,
189 - ]
190 - );
191 - $this->setOAuth($oauth);
192 - } else {
193 - $authMethod = $this->getSendingMethodSettings('smtp_method');
194 - if (in_array($authMethod, ['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'])) {
195 - $this->AuthType = $authMethod;
196 - }
197 - if ($this->SMTPAuth) {
198 - $this->Password = trim($this->getSendingMethodSettings('smtp_password'));
199 - }
245 + $authMethod = $this->getSendingMethodSettings('smtp_method');
246 + if (in_array($authMethod, ['CRAM-MD5', 'LOGIN', 'PLAIN'])) {
247 + $this->AuthType = $authMethod;
200 248 }
249 + if ($this->SMTPAuth) {
250 + $this->Password = trim($this->getSendingMethodSettings('smtp_password'));
251 + }
201 252
202 - $this->SMTPSecure = trim((string)$this->getSendingMethodSettings('smtp_secured'));
253 + $this->SMTPSecure = trim($this->getSendingMethodSettings('smtp_secured'));
203 254
204 255 if (empty($this->Sender)) {
205 256 $this->Sender = strpos($this->Username, '@') ? $this->Username : $this->getSendingMethodSettings('from_email');
206 257 }
@@ -236,8 +287,11 @@
236 287 $this->isMail();
237 288 }
238 289 }
239 290
291 + /**
292 + * Dynamically called from PHPMailer
293 + */
240 294 protected function externalSend(string $MIMEHeader, string $MIMEBody): bool
241 295 {
242 296 $fromName = empty($this->FromName) ? $this->config->get('from_name') : $this->FromName;
243 297 $reply_to = array_shift($this->ReplyTo);
@@ -297,8 +351,9 @@
297 351 return false;
298 352 }
299 353 }
300 354
355 + //Check if there is at least one reply to otherwise add the default one.
301 356 if (empty($this->ReplyTo) && empty($this->ReplyToQueue)) {
302 357 if (!empty($this->replyemail)) {
303 358 $replyToEmail = $this->replyemail;
304 359 } elseif ($this->config->get('from_as_replyto', 1) == 1) {
@@ -317,8 +372,9 @@
317 372
318 373 $this->acymAddReplyTo($replyToEmail, $replyToName);
319 374 }
320 375
376 + // Embed images if there are images to embed
321 377 $shouldEmbed = $this->config->get('embed_images', 0);
322 378 if (intval($shouldEmbed) === 1 && $this->Mailer !== 'elasticemail') {
323 379 $this->embedImages();
324 380 }
@@ -344,8 +400,9 @@
344 400
345 401 return false;
346 402 }
347 403
404 + //Check the from address
348 405 if (empty($this->From) || !acym_isValidEmail($this->From)) {
349 406 $this->reportMessage = acym_translation('ACYM_VALID_EMAIL').' ( '.acym_translation('ACYM_FROM_EMAIL').' : '.$this->From.' ) ';
350 407 $this->errorNumber = 9;
351 408 if ($this->report) {
@@ -369,10 +426,13 @@
369 426 if ($this->config->get('save_body') === '1') {
370 427 @file_put_contents(ACYM_ROOT.'acydebug_mail.html', $this->Body);
371 428 }
372 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!
373 432 $this->Body = htmlentities($this->Body);
374 433 $this->Body = htmlspecialchars_decode($this->Body);
434 + //Fix The Bat issues for special encoding as &sigmaf; was interpreted as &sigma;f;...
375 435 $this->Body = str_replace(['&amp;', '&sigmaf;'], ['&', 'ς'], $this->Body);
376 436
377 437 if ($this->CharSet !== 'utf-8') {
378 438 $this->Body = $this->encodingHelper->change($this->Body, 'UTF-8', $this->CharSet);
@@ -378,8 +438,9 @@
378 438 $this->Body = $this->encodingHelper->change($this->Body, 'UTF-8', $this->CharSet);
379 439 $this->Subject = $this->encodingHelper->change($this->Subject, 'UTF-8', $this->CharSet);
380 440 }
381 441
442 + // These characters can break the send process, let's remove them from the subject
382 443 $this->Subject = str_replace(
383 444 ['’', '“', '”', '–'],
384 445 ["'", '"', '"', '-'],
385 446 $this->Subject
@@ -384,8 +445,9 @@
384 445 ["'", '"', '"', '-'],
385 446 $this->Subject
386 447 );
387 448
449 + // BE CAREFUL! This space is not a space, it's a ALT0160 chr(194) by char(32) which means almost &nbsp;
388 450 $this->Body = str_replace(" ", ' ', $this->Body);
389 451
390 452 $externalSending = false;
391 453
@@ -416,8 +478,9 @@
416 478 }
417 479 }
418 480
419 481
482 + //display error if bloque is displayed... for free.fr especially
420 483 if (!empty($warnings) && strpos($warnings, 'bloque')) {
421 484 $result = false;
422 485 }
423 486
@@ -437,8 +500,9 @@
437 500 $this->reportMessage .= " \n\n ".$warnings;
438 501 }
439 502 $this->errorNumber = 1;
440 503 if ($this->report) {
504 + //We display the report here... we add a link to our doc for the "could not instantiate mail function".
441 505 $this->reportMessage = str_replace(
442 506 'Could not instantiate mail function',
443 507 '<a target="_blank" href="'.ACYM_DOCUMENTATION.'faq/could-not-instantiate-mail-function">'.acym_translation('ACYM_COUND_NOT_INSTANCIATE_MAIL_FUCNTION').'</a>',
444 508 $this->reportMessage
@@ -485,8 +549,9 @@
485 549 }
486 550
487 551 public function load(int $mailId, ?object $user = null): ?object
488 552 {
553 + // If it's already loaded return the email
489 554 if (isset($this->defaultMail[$mailId])) {
490 555 return $this->defaultMail[$mailId];
491 556 }
492 557
@@ -525,8 +590,9 @@
525 590 $acymLanguages['userLanguage'] = $this->userLanguage;
526 591 $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
527 592 }
528 593
594 + //We could not load the email...
529 595 if (empty($this->defaultMail[$mailId]->id)) {
530 596 unset($this->defaultMail[$mailId]);
531 597
532 598 return null;
@@ -534,14 +600,14 @@
534 600
535 601 if (!empty($this->defaultMail[$mailId]->attachments)) {
536 602 $this->defaultMail[$mailId]->attach = [];
537 603
538 - $attachments = json_decode($this->defaultMail[$mailId]->attachments);
604 + $attachments = json_decode($this->defaultMail[$mailId]->attachments, true);
539 605 foreach ($attachments as $oneAttach) {
540 606 $attach = new \stdClass();
541 - $attach->name = basename($oneAttach->filename);
542 - $attach->filename = str_replace(['/', '\\'], DS, ACYM_ROOT).$oneAttach->filename;
543 - $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'];
544 610 $this->defaultMail[$mailId]->attach[] = $attach;
545 611 }
546 612 }
547 613
@@ -547,8 +613,11 @@
547 613
548 614
549 615 $this->mailId = $mailId;
550 616 $this->id = $this->mailId;
617 + if (empty($this->defaultMail[$mailId]->language)) {
618 + $this->defaultMail[$mailId]->language = acym_getDefaultNewsletterLanguage();
619 + }
551 620 $this->mail = clone $this->defaultMail[$mailId];
552 621 acym_trigger('replaceContent', [&$this->defaultMail[$mailId], true]);
553 622
554 623 $this->prepareEmailContent($mailId);
@@ -558,8 +627,16 @@
558 627
559 628 return $this->defaultMail[$mailId];
560 629 }
561 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 + */
562 639 public function sendOne($mailId, $user, array $options = []): bool
563 640 {
564 641 $this->clearAll();
565 642
@@ -568,9 +645,9 @@
568 645 $this->dtextsFailed = false;
569 646
570 647 if (empty($receiver->email)) {
571 648 $this->errorNumber = 4;
572 - $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>');
573 650 if ($this->report) {
574 651 acym_enqueueMessage($this->reportMessage, 'error');
575 652 }
576 653
@@ -576,11 +653,13 @@
576 653
577 654 return false;
578 655 }
579 656
657 + // Can be a string for Joomla 3 and WordPress
580 658 $mailId = intval($mailId);
581 659 $receiver->id = intval($receiver->id);
582 660
661 + // Load the mail with global tags replaced
583 662 if (!$this->load($mailId, $receiver)) {
584 663 $this->reportMessage = 'Can not load the email with ID n°'.$mailId;
585 664 $this->errorNumber = 2;
586 665
@@ -597,14 +676,14 @@
597 676 $this->prepareBcc($mailId, $options);
598 677 $this->prepareAttachments($mailId, $options);
599 678 $this->replaceDynamicContent($mailId);
600 679 $this->prepareTracking($mailId, $receiver);
601 -
602 680 if (!$this->handleDtexts($receiver)) {
603 681 $this->dtextsFailed = true;
604 682
605 683 return false;
606 684 }
685 +
607 686 $status = $this->send();
608 687
609 688 $this->initStatistics($status, $receiver->id);
610 689
@@ -645,9 +724,9 @@
645 724
646 725 $followUpClass->triggerFollowUp(intval($followUp->id), intval($userId), $delay, $mailToSkip);
647 726 }
648 727
649 - 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
650 729 {
651 730 if (!$fromStat && !in_array($this->type, MailClass::TYPES_WITH_STATS)) {
652 731 return;
653 732 }
@@ -668,9 +747,9 @@
668 747
669 748 $utmCampaign = acym_getAlias($mail->subject);
670 749 }
671 750
672 - 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);
673 752 if (empty($results)) {
674 753 return;
675 754 }
676 755
@@ -681,8 +760,9 @@
681 760 if ($countLinks[$url] === 1) {
682 761 continue;
683 762 }
684 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
685 765 $previousIsOutlook = false;
686 766 if (strpos($results[0][$key], '<v:roundrect') === 0) {
687 767 $previousLinkHandled = $results[0][$key];
688 768 if ($countLinks[$url] === 2) {
@@ -720,8 +800,9 @@
720 800
721 801 foreach ($results[1] as $i => $url) {
722 802 $urlsNotToTrack = [
723 803 'task=unsub',
804 + 'task=disableTracking',
724 805 'fonts.googleapis.com',
725 806 ];
726 807
727 808 $track = true;
@@ -732,22 +813,30 @@
732 813 break;
733 814 }
734 815 }
735 816
817 + //We don't track unsubscribe link
736 818 if (isset($urls[$results[0][$i]]) || !$track) {
737 819 continue;
738 820 }
739 821
822 + //We often need to check if the url is within the website... but we don't care about http or https
740 823 $simplifiedUrl = str_replace(['https://', 'http://', 'www.'], '', $url);
741 824 $simplifiedWebsite = str_replace(['https://', 'http://', 'www.'], '', ACYM_LIVE);
742 825 $internalUrl = strpos($simplifiedUrl, rtrim($simplifiedWebsite, '/')) === 0;
743 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
744 829 $subfolder = false;
745 830 if ($internalUrl) {
746 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
747 834 if (strpos($urlWithoutBase, '/') || strpos($urlWithoutBase, '?')) {
835 + // Get the supposed sub-folder name
748 836 $slashPosition = strpos($urlWithoutBase, '/');
749 837 $folderName = substr($urlWithoutBase, 0, !$slashPosition ? strpos($urlWithoutBase, '?') : $slashPosition);
838 + //There is no dot in a folder!
750 839 if (strpos($folderName, '.') === false) {
751 840 $subfolder = @is_dir(ACYM_ROOT.$folderName);
752 841 }
753 842 }
@@ -762,8 +851,9 @@
762 851 $args = [];
763 852 $args[] = empty($campaign->sending_params['utm_source']) ? 'utm_source=newsletter_'.$idToUse : 'utm_source='.urlencode($campaign->sending_params['utm_source']);
764 853 $args[] = empty($campaign->sending_params['utm_medium']) ? 'utm_medium=email' : 'utm_medium='.urlencode($campaign->sending_params['utm_medium']);
765 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
766 856 $anchor = '';
767 857 if (strpos($url, '#') !== false) {
768 858 $anchor = substr($url, strpos($url, '#'));
769 859 $url = substr($url, 0, strpos($url, '#'));
@@ -773,40 +863,30 @@
773 863 $mytracker = $url.'&'.implode('&', $args);
774 864 } else {
775 865 $mytracker = $url.'?'.implode('&', $args);
776 866 }
867 + //We add back the anchor if we had one
777 868 $mytracker .= $anchor;
778 869 $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
779 870
871 + //Set the url variable so that we can use it later on...
780 872 $url = $mytracker;
781 873 }
782 874
783 875 if (strpos($trackingSystem, 'acymailing') !== false) {
784 - $isAutologin = false;
785 - $autologinParams = 'autoSubId=%7Bsubscriber:id%7D&amp;subKey=%7Bsubscriber:key%7Curlencode%7D';
786 - if (strpos($url, $autologinParams) !== false) {
787 - $isAutologin = true;
788 - $url = str_replace(
789 - [
790 - '?'.$autologinParams.'&amp;',
791 - '?'.$autologinParams,
792 - '&amp;'.$autologinParams,
793 - ],
794 - [
795 - '?',
796 - '',
797 - '',
798 - ],
799 - $url
800 - );
876 + $isAutologin = strpos($url, self::AUTOLOGIN_PARAMS) !== false;
877 + if ($isAutologin) {
878 + $url = $this->stripAutologinParams($url);
801 879 }
802 880
803 - if (preg_match('#passw|modify|\{|%7B#i', $url)) {
881 + if (preg_match('#passw|modify#i', $url)) {
804 882 continue;
805 883 }
806 884
885 + $urlForRegistration = $url;
886 +
807 887 if (!$fromStat) {
808 - $mytracker = $urlClass->getUrl($url, $mailId, $userid);
888 + $mytracker = $urlClass->getUrl($urlForRegistration, $mailId, $userid, $userkey);
809 889 }
810 890
811 891 if (empty($mytracker)) {
812 892 continue;
@@ -812,10 +892,10 @@
812 892 continue;
813 893 }
814 894
815 895 if ($isAutologin) {
816 - $mytracker .= strpos($mytracker, '?') === false ? '?' : '&amp;';
817 - $mytracker .= $autologinParams;
896 + $separator = strpos($mytracker, '?') === false ? '?' : '&amp;';
897 + $mytracker .= $separator.self::AUTOLOGIN_PARAMS;
818 898 }
819 899
820 900 $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
821 901 }
@@ -825,12 +905,17 @@
825 905 }
826 906
827 907 public function textVersion(string $html, bool $fullConvert = true): string
828 908 {
909 + //Replace relative links into absolute before replacing the text version so that we keep correct urls
829 910 $html = acym_absoluteURL($html);
830 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
831 914 if ($fullConvert) {
915 + //As in HTML multiple spaces are interpreted as only one space, we remove the multiple spaces for the text version
832 916 $html = preg_replace('# +#', ' ', $html);
917 + //Same thing, return chars don't exist in html so we can simply remove them, neither \t
833 918 $html = str_replace(["\n", "\r", "\t"], '', $html);
834 919 }
835 920
836 921 $removepictureslinks = "#< *a[^>]*> *< *img[^>]*> *< *\/ *a *>#isU";
@@ -858,15 +943,20 @@
858 943 ['', '', '', '', "\n\n", "\n* ", "\n", "\n", '${2} ( ${1} )'],
859 944 $html
860 945 );
861 946
947 + //The striptags function may not do the job properly in some cases...
862 948 $text = preg_replace('#(&lt;|&\#60;)([^ \n\r\t])#i', '&lt; ${2}', $text);
863 949
864 - $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 :) :) :)
865 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.
866 955 $text = trim(@html_entity_decode($text, ENT_QUOTES, 'UTF-8'));
867 956
868 957 if ($fullConvert) {
958 + //We do that one more time as some extra spaces may have appeared
869 959 $text = preg_replace('# +#', ' ', $text);
870 960 $text = preg_replace('#\n *\n\s+#', "\n\n", $text);
871 961 }
872 962
@@ -872,8 +962,11 @@
872 962
873 963 return $text;
874 964 }
875 965
966 + /**
967 + * @throws Exception
968 + */
876 969 protected function embedImages(): bool
877 970 {
878 971 preg_match_all('/(src|background)=[\'|"]([^"\']*)[\'|"]/Ui', $this->Body, $images);
879 972
@@ -896,12 +989,15 @@
896 989
897 990 $allImages = [];
898 991
899 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
900 995 if (isset($allImages[$url])) {
901 996 continue;
902 997 }
903 998
999 + // Don't embed images with a controller, most likely the stats picture
904 1000 if (strpos($url, 'ctrl=') !== false) {
905 1001 continue;
906 1002 }
907 1003
@@ -906,25 +1002,30 @@
906 1002 }
907 1003
908 1004 $allImages[$url] = 1;
909 1005
1006 + // We convert the url into local directory
910 1007 $path = acym_internalUrlToPath($url);
911 1008 $path = $this->removeAdditionalParams($path);
912 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 +
913 1021 $filename = str_replace(['%', ' '], '_', basename($url));
914 1022 $filename = $this->removeAdditionalParams($filename);
915 1023
916 1024 $md5 = md5($filename);
917 1025 $cid = 'cid:'.$md5;
918 - $fileParts = explode(".", $filename);
919 - if (empty($fileParts[1])) {
920 - continue;
921 - }
922 - $ext = strtolower($fileParts[1]);
923 - if (!isset($mimetypes[$ext])) {
924 - continue;
925 - }
926 1026
1027 + // We only change the url if we were able to embed the image.
927 1028 if ($this->addEmbeddedImage($path, $md5, $filename, 'base64', $mimetypes[$ext])) {
928 1029 $this->Body = preg_replace('/'.preg_quote($images[0][$i], '/').'/Ui', $images[1][$i].'="'.$cid.'"', $this->Body);
929 1030 } else {
930 1031 $embedSuccess = false;
@@ -933,32 +1034,42 @@
933 1034
934 1035 return $embedSuccess;
935 1036 }
936 1037
937 - public function cleanText(?string $text): string
1038 + private function getEmbeddableImagePath(string $path): string
938 1039 {
939 - return trim(preg_replace('/(%0A|%0D|\n+|\r+)/i', '', (string)$text));
940 - }
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 + }
941 1044
942 - public function addParamInfo(): void
943 - {
944 - if (!empty($_SERVER)) {
945 - $serverinfo = [];
946 - foreach ($_SERVER as $oneKey => $oneInfo) {
947 - $serverinfo[] = $oneKey.' => '.strip_tags(print_r($oneInfo, true));
948 - }
949 - $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 '';
950 1049 }
951 1050
952 - if (!empty($_REQUEST)) {
953 - $postinfo = [];
954 - foreach ($_REQUEST as $oneKey => $oneInfo) {
955 - $postinfo[] = $oneKey.' => '.strip_tags(print_r($oneInfo, true));
956 - }
957 - $this->addParam('postinfo', implode('<br />', $postinfo));
1051 + if (!is_file($path) || !is_readable($path)) {
1052 + return '';
958 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;
959 1062 }
960 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 + */
961 1072 public function addParam(string $name, $value): void
962 1073 {
963 1074 $tagName = '{'.$name.'}';
964 1075 $this->parameters[$tagName] = $value;
@@ -970,12 +1081,14 @@
970 1081
971 1082 $overrideClass = new OverrideClass();
972 1083 $override = $overrideClass->getMailByBaseContent($options['subject'], $options['message']);
973 1084
1085 + // We let the CMS send the email
974 1086 if (empty($override) && !$overrideAllEmails) {
975 1087 return false;
976 1088 }
977 1089
1090 + // AcyMailing needs to send the email
978 1091 $this->autoAddUser = true;
979 1092
980 1093 if (empty($override)) {
981 1094 $this->trackEmail = false;
@@ -987,8 +1100,33 @@
987 1100 return $this->sendOverride($override, $options);
988 1101 }
989 1102 }
990 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 +
991 1129 private function getSendingMethodSettings(string $settingName, string $default = ''): string
992 1130 {
993 1131 if (empty($this->currentMethodSetting)) {
994 1132 return $this->config->get($settingName, $default);
@@ -1083,36 +1221,32 @@
1083 1221
1084 1222 return true;
1085 1223 }
1086 1224
1225 + /**
1226 + * @param mixed $user
1227 + *
1228 + * @throws Exception
1229 + */
1087 1230 private function loadUser($user, bool $force = false): object
1088 1231 {
1089 - if (is_string($user) && strpos($user, '@')) {
1090 - $receiver = $this->userClass->getOneByEmail($user);
1091 -
1092 - if (empty($receiver)) {
1093 - if (($force || $this->autoAddUser) && acym_isValidEmail($user)) {
1094 - $newUser = new \stdClass();
1095 - $newUser->email = $user;
1096 - $this->userClass->checkVisitor = false;
1097 - $this->userClass->sendConf = false;
1098 - acym_setVar('acy_source', 'When sending a test');
1099 - $this->userClass->triggers = $this->userCreationTriggers;
1100 - $userId = $this->userClass->save($newUser);
1101 - $receiver = $this->userClass->getOneById($userId);
1102 - } else {
1103 - throw new Exception(acym_translation('ACYM_USER_NOT_FOUND'));
1104 - }
1105 - }
1232 + if (is_numeric($user)) {
1233 + $receiver = $this->userClass->getOneById($user);
1106 1234 } elseif (is_object($user)) {
1107 1235 $receiver = $user;
1108 - } else {
1109 - $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);
1110 1240 }
1111 1241
1112 - $this->userLanguage = empty($receiver->language) ? acym_getLanguageTag() : $receiver->language;
1113 - $this->receiverEmail = $receiver->email;
1242 + if (empty($receiver)) {
1243 + throw new Exception(esc_attr(acym_translation('ACYM_USER_NOT_FOUND')));
1244 + }
1114 1245
1246 + $this->userLanguage = empty($receiver->language) ? acym_getDefaultNewsletterLanguage() : $receiver->language;
1247 + $this->receiverEmail = (string)($receiver->email ?? '');
1248 +
1115 1249 return $receiver;
1116 1250 }
1117 1251
1118 1252 private function prepareContent(int $mailId, array $options): void
@@ -1129,17 +1263,19 @@
1129 1263 if ($this->isTest && !empty($options['testNote'])) {
1130 1264 $this->Body = '<div style="text-align: center; padding: 25px; font-family: Poppins; font-size: 20px">'.$options['testNote'].'</div>'.$this->Body;
1131 1265 }
1132 1266
1267 + //We add the intro text at the top of the body
1133 1268 if (!empty($this->introtext)) {
1134 1269 $this->Body = $this->introtext.$this->Body;
1135 1270 }
1136 1271
1137 1272 if (!empty($this->defaultMail[$mailId]->preheader)) {
1138 - $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;">';
1139 - $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;
1140 1275 $preHeader .= '</div><!--<![endif]-->';
1141 1276
1277 + //We want to insert the preview at the start of the body so we match the start of the mail
1142 1278 preg_match('#(<(.*)<body(.*)>)#Uis', $this->Body, $matches);
1143 1279 if (empty($matches) || empty($matches[1])) {
1144 1280 $this->Body = $preHeader.$this->Body;
1145 1281 } else {
@@ -1155,9 +1291,10 @@
1155 1291 }
1156 1292
1157 1293 private function prepareHeaders(int $mailId, int $receiverId, array $options): void
1158 1294 {
1159 - $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));
1160 1297 $this->MessageID = '<'.preg_replace('|[^a-z0-9+_]|i', '', $subject).'@'.$this->serverHostname().'>';
1161 1298 $this->addCustomHeader('Feedback-ID', $this->defaultMail[$mailId]->id.':'.$receiverId.':'.$this->defaultMail[$mailId]->type.':'.base64_encode(ACYM_ROOT));
1162 1299
1163 1300 if (!empty($this->defaultMail[$mailId]->headers)) {
@@ -1175,8 +1312,9 @@
1175 1312 {
1176 1313 $receiverName = '';
1177 1314 if ($this->config->get('add_names', true)) {
1178 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
1179 1317 if ($receiverName == $this->cleanText($receiver->email)) {
1180 1318 $receiverName = '';
1181 1319 }
1182 1320 }
@@ -1184,8 +1322,9 @@
1184 1322
1185 1323 $this->setFrom($this->getSendSettings('from_email', $mailId), $this->getSendSettings('from_name', $mailId));
1186 1324 $this->acymAddReplyTo($this->defaultMail[$mailId]->reply_to_email ?? '', $this->defaultMail[$mailId]->reply_to_name ?? '');
1187 1325
1326 + // This is the bounce email address
1188 1327 if (!empty($this->defaultMail[$mailId]->bounce_email)) {
1189 1328 $this->Sender = $this->cleanText($this->defaultMail[$mailId]->bounce_email);
1190 1329 } else {
1191 1330 $this->Sender = $this->cleanText($this->config->get('bounce_email'));
@@ -1191,8 +1330,11 @@
1191 1330 $this->Sender = $this->cleanText($this->config->get('bounce_email'));
1192 1331 }
1193 1332 }
1194 1333
1334 + /**
1335 + * @throws Exception
1336 + */
1195 1337 private function prepareBcc(int $mailId, array $options): void
1196 1338 {
1197 1339 $bccAddresses = [];
1198 1340
@@ -1206,12 +1348,20 @@
1206 1348 }
1207 1349
1208 1350 if (!empty($bccAddresses)) {
1209 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 +
1210 1357 foreach ($bccAddresses as $oneBccAddress) {
1211 1358 $this->loadUser($oneBccAddress, true);
1212 1359 $this->addBCC($oneBccAddress);
1213 1360 }
1361 +
1362 + $this->receiverEmail = $receiverEmail;
1363 + $this->userLanguage = $userLanguage;
1214 1364 }
1215 1365 }
1216 1366
1217 1367 private function prepareAttachments(int $mailId, array $options): void
@@ -1252,8 +1402,9 @@
1252 1402 private function replaceDynamicContent(int $mailId): void
1253 1403 {
1254 1404 $this->replaceParams();
1255 1405
1406 + // Left side are mail columns on which we replace the short codes
1256 1407 $this->body = &$this->Body;
1257 1408 $this->altbody = &$this->AltBody;
1258 1409 $this->subject = &$this->Subject;
1259 1410 $this->from = &$this->From;
@@ -1268,8 +1419,17 @@
1268 1419 $this->stylesheet = &$this->stylesheet;
1269 1420 $this->links_language = $this->defaultMail[$mailId]->links_language;
1270 1421 }
1271 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 +
1272 1432 private function prepareTracking(int $mailId, object $receiver): void
1273 1433 {
1274 1434 if ($this->canTrack($mailId, $receiver)) {
1275 1435 $receiver->id = intval($receiver->id);
@@ -1274,9 +1434,9 @@
1274 1434 if ($this->canTrack($mailId, $receiver)) {
1275 1435 $receiver->id = intval($receiver->id);
1276 1436 $this->statPicture($this->mailId, $receiver->id);
1277 1437 $this->body = acym_absoluteURL($this->body);
1278 - $this->statClick($this->mailId, $receiver->id);
1438 + $this->statClick($this->mailId, $receiver->id, false, $receiver->key ?? '');
1279 1439 if (acym_isTrackingSalesActive()) {
1280 1440 $this->trackingSales($this->mailId, $receiver->id);
1281 1441 }
1282 1442 }
@@ -1285,8 +1445,9 @@
1285 1445 private function handleDtexts(object $receiver): bool
1286 1446 {
1287 1447 $this->replaceParams();
1288 1448
1449 + // Sending a spam-test, use the current user instead
1289 1450 if (strpos($receiver->email, '@mt.acyba.com') !== false) {
1290 1451 $currentUser = $this->userClass->getOneByEmail(acym_currentUserEmail());
1291 1452 if (empty($currentUser)) {
1292 1453 $currentUser = $receiver;
@@ -1309,8 +1470,9 @@
1309 1470 if (!empty($acymLanguages['userLanguage'])) {
1310 1471 unset($acymLanguages['userLanguage']);
1311 1472 }
1312 1473
1474 + // A user based dtext may have inserted HTML content, we need to re-inline the CSS to it
1313 1475 foreach ($result as $oneResult) {
1314 1476 if (!empty($oneResult['emogrifier'])) {
1315 1477 $this->prepareEmailContent();
1316 1478 break;
@@ -1317,9 +1479,10 @@
1317 1479 }
1318 1480 }
1319 1481
1320 1482 if ($this->config->get('multiple_part', false)) {
1321 - $this->altbody = $this->textVersion($this->Body);
1483 + $bodyForText = preg_replace('#<head\b[^>]*>.*?</head\s*>#is', '', $this->Body);
1484 + $this->altbody = $this->textVersion($bodyForText);
1322 1485 }
1323 1486
1324 1487 $this->replaceParams();
1325 1488
@@ -1366,9 +1529,9 @@
1366 1529 private function statPicture(int $mailId, int $userId): void
1367 1530 {
1368 1531 $pictureLink = acym_frontendLink('frontstats&task=openStats&id='.$mailId.'&userid='.$userId, true, false);
1369 1532
1370 - $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" />';
1371 1534
1372 1535 if (strpos($this->body, '</body>')) {
1373 1536 $this->body = str_replace('</body>', $statPicture.'</body>', $this->body);
1374 1537 } else {
@@ -1393,8 +1556,9 @@
1393 1556 }
1394 1557
1395 1558 $pluginHelper = new PluginHelper();
1396 1559
1560 + //We create an extra tag which contains all possible parameters...
1397 1561 $this->generateAllParams();
1398 1562
1399 1563 $vars = [
1400 1564 'Subject',
@@ -1419,12 +1583,16 @@
1419 1583 }
1420 1584 }
1421 1585 }
1422 1586
1587 + /**
1588 + * Create a new parameter called {alltags} which will include all the others
1589 + */
1423 1590 private function generateAllParams(): void
1424 1591 {
1425 1592 $result = '<table style="border:1px solid;border-collapse:collapse;" border="1" cellpadding="10"><tr><td>Tag</td><td>Value</td></tr>';
1426 1593 foreach ($this->parameters as $name => $value) {
1594 + //Just in case of...
1427 1595 if (!is_string($value)) continue;
1428 1596
1429 1597 $result .= '<tr><td>'.trim($name, '{}').'</td><td>'.$value.'</td></tr>';
1430 1598 }
@@ -1433,11 +1601,13 @@
1433 1601 }
1434 1602
1435 1603 private function sendOverride(object $override, array $options): bool
1436 1604 {
1437 - for ($i = 1 ; $i < count($override->parameters) ; $i++) {
1605 + // 2 - Prepare the email and params
1606 + for ($i = 1; $i < count($override->parameters); $i++) {
1438 1607 $oneParam = $override->parameters[$i];
1439 1608
1609 + // Joomla emails have links as text, convert them
1440 1610 $unmodified = $oneParam;
1441 1611 $oneParam = preg_replace(
1442 1612 '/(http|https):\/\/(.*)/',
1443 1613 '<a href="$1://$2" target="_blank">$1://$2</a>',
@@ -1452,11 +1622,13 @@
1452 1622 }
1453 1623
1454 1624 $this->addParam('subject', $options['subject']);
1455 1625
1626 + // 3 - Send the email
1456 1627 $this->overrideEmailToSend = $override;
1457 1628 $statusSend = $this->sendOne($override->id, $options['to'], $options);
1458 1629 if (!$statusSend && !empty($this->reportMessage)) {
1630 + // Something went wrong when trying to send the override, log the information in the cron logs file
1459 1631 $cronHelper = new CronHelper();
1460 1632 $cronHelper->saveReport([$this->reportMessage]);
1461 1633 }
1462 1634
@@ -1462,21 +1634,20 @@
1462 1634
1463 1635 return $statusSend;
1464 1636 }
1465 1637
1466 - private function getSendSettings(string $type, int $mailId = 0): string
1638 + public function getSendSettings(string $type, int $mailId = 0): string
1467 1639 {
1468 - if (!in_array($type, ['from_name', 'from_email', 'replyto_name', 'replyto_email'])) {
1640 + if (!in_array($type, ['from_name', 'from_email', 'replyto_name', 'replyto_email', 'bounce_email'])) {
1469 1641 return '';
1470 1642 }
1471 1643
1472 1644 $mailType = strpos($type, 'replyto') !== false ? str_replace('replyto', 'reply_to', $type) : $type;
1473 -
1474 - if (!empty($mailId) && !empty($this->defaultMail[$mailId]) && !empty($this->defaultMail[$mailId]->$mailType)) {
1645 + if (!empty($mailId) && !empty($this->defaultMail[$mailId]->$mailType)) {
1475 1646 return $this->defaultMail[$mailId]->$mailType;
1476 1647 }
1477 1648
1478 - $lang = empty($this->userLanguage) ? acym_getLanguageTag() : $this->userLanguage;
1649 + $lang = empty($this->userLanguage) ? acym_getDefaultNewsletterLanguage() : $this->userLanguage;
1479 1650 $setting = $this->config->get($type);
1480 1651 $translation = $this->config->get('sender_info_translation');
1481 1652
1482 1653 if (!empty($translation)) {
@@ -1481,9 +1652,9 @@
1481 1652
1482 1653 if (!empty($translation)) {
1483 1654 $translation = json_decode($translation, true);
1484 1655
1485 - if (!empty($translation[$lang])) {
1656 + if (!empty($translation[$lang][$type])) {
1486 1657 $setting = $translation[$lang][$type];
1487 1658 }
1488 1659 }
1489 1660
@@ -1505,16 +1676,22 @@
1505 1676
1506 1677 private function inlineCSS(int $mailId): void
1507 1678 {
1508 1679 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1680 + if (empty($mail->body)) {
1681 + return;
1682 + }
1509 1683
1510 - global $emogrifiedMediaCSS;
1511 - $emogrifiedMediaCSS = '';
1684 + global $acymailingEmogrifiedMediaCSS;
1685 + $acymailingEmogrifiedMediaCSS = '';
1512 1686
1513 1687 $style = $this->getEmailStylesheet($mail);
1514 - $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));
1515 1691 $domDocument = $cssInliner->getDomDocument();
1516 1692
1693 + // Remove the TinyMCE ids and the trailing ; in styles
1517 1694 $mail->body = preg_replace(
1518 1695 [
1519 1696 '# id="mce_\d+"#Ui',
1520 1697 '#(style="[^"]+);"#Ui',
@@ -1527,10 +1704,12 @@
1527 1704 ->removeRedundantClassesAfterCssInlined($cssInliner)
1528 1705 ->renderBodyContent()
1529 1706 );
1530 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
1531 1709 $mail->body = str_replace('&zwj;', '', $mail->body);
1532 1710
1711 + // Remove the extra spaces in the style attributes
1533 1712 $mail->body = preg_replace_callback(
1534 1713 '#style="([^"]+)"#Ui',
1535 1714 function ($matches) {
1536 1715 return 'style="'.str_replace(['; ', ': '], [';', ':'], $matches[1]).'"';
@@ -1549,10 +1728,13 @@
1549 1728 $finalContent .= '<meta http-equiv="Content-Type" content="text/html; charset='.strtolower($this->config->get('charset')).'" />'."\n";
1550 1729 $finalContent .= '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'."\n";
1551 1730 $finalContent .= '<title>'.$mail->subject.'</title>'."\n";
1552 1731
1553 - global $emogrifiedMediaCSS;
1554 - $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>';
1555 1737 $finalContent .= '<!--[if mso]><style type="text/css">#acym__wysid__template center > table { width: 580px; }</style><![endif]-->';
1556 1738 $finalContent .= '<!--[if !mso]><!--><style>#acym__wysid__template center > table { width: 100%; }</style><!--<![endif]-->';
1557 1739
1558 1740 if (!empty($mail->headers)) {
@@ -1579,12 +1761,14 @@
1579 1761 private function getEmailStylesheet(object $mail): array
1580 1762 {
1581 1763 $style = [];
1582 1764
1765 + // If this is a drag and drop mail we add foundation css for email
1583 1766 if (strpos($mail->body, 'acym__wysid__template') !== false) {
1584 1767 static $foundationCSS = null;
1585 1768 if (empty($foundationCSS)) {
1586 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
1587 1771 $foundationCSS = str_replace('#acym__wysid__template ', '', $foundationCSS);
1588 1772 }
1589 1773 $style['foundation'] = $foundationCSS;
1590 1774 }
@@ -1626,8 +1810,9 @@
1626 1810 }
1627 1811
1628 1812 $statusSend = $this->sendOne($this->overrideEmailToSend->id, $options['to'], $options);
1629 1813 if (!$statusSend && !empty($this->reportMessage)) {
1814 + // Something went wrong when trying to send the override, log the information in the cron logs file
1630 1815 $cronHelper = new CronHelper();
1631 1816 $cronHelper->addMessage($this->reportMessage);
1632 1817 $cronHelper->saveReport();
1633 1818 }
@@ -1634,9 +1819,17 @@
1634 1819
1635 1820 return $statusSend;
1636 1821 }
1637 1822
1823 + /* * * * * * * * * * * * * * * * *
1824 + * *
1825 + * Override PHPMailer's methods *
1826 + * *
1827 + * * * * * * * * * * * * * * * * */
1638 1828
1829 + /**
1830 + * @throws Exception
1831 + */
1639 1832 private function acymAddReplyTo(string $email, string $name): void
1640 1833 {
1641 1834 if (empty($email)) {
1642 1835 return;
@@ -1645,8 +1838,9 @@
1645 1838 $replyToName = $this->config->get('add_names', true) ? $this->cleanText(trim($name)) : '';
1646 1839 $replyToEmail = trim($email);
1647 1840
1648 1841 if (substr_count($replyToEmail, '@') > 1) {
1842 + //We have more than one reply to...
1649 1843 $replyToEmailArray = explode(';', str_replace([';', ','], ';', $replyToEmail));
1650 1844 $replyToNameArray = explode(';', str_replace([';', ','], ';', $replyToName));
1651 1845 foreach ($replyToEmailArray as $i => $oneReplyTo) {
1652 1846 $this->addReplyTo($this->cleanText($oneReplyTo), @$replyToNameArray[$i]);
@@ -1665,8 +1859,13 @@
1665 1859 $this->FromName = $this->cleanText($name);
1666 1860 }
1667 1861 }
1668 1862
1863 + /**
1864 + * Outputs debugging info via user-defined method
1865 + *
1866 + * @param string $str
1867 + */
1669 1868 protected function edebug($str)
1670 1869 {
1671 1870 if (strpos($this->ErrorInfo, $str) === false) {
1672 1871 $this->ErrorInfo .= ' '.$str;
@@ -1676,8 +1875,9 @@
1676 1875 public function getMailMIME()
1677 1876 {
1678 1877 $result = parent::getMailMIME();
1679 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
1680 1880 $result = rtrim($result, static::$LE);
1681 1881
1682 1882 if ($this->Mailer != 'mail') {
1683 1883 $result .= static::$LE.static::$LE;