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 +856 -650 10.0.1 → 11.1.0 View file →
@@ -1,10 +1,10 @@
1 1 <?php
2 2
3 3 namespace AcyMailing\Helpers;
4 4
5 -
6 5 use AcyMailing\Classes\CampaignClass;
6 +use AcyMailing\Classes\ConfigurationClass;
7 7 use AcyMailing\Classes\FollowupClass;
8 8 use AcyMailing\Classes\MailArchiveClass;
9 9 use AcyMailing\Classes\MailClass;
10 10 use AcyMailing\Classes\OverrideClass;
@@ -12,16 +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 - public $XMailer = ' ';
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
22 - public $SMTPAutoTLS = false;
23 -
25 + // Redefine PHPMailer protected attributes for dynamic texts system
24 26 public $to = [];
25 27 public $cc = [];
26 28 public $bcc = [];
27 29 public $ReplyTo = [];
@@ -26,79 +28,90 @@
26 28 public $bcc = [];
27 29 public $ReplyTo = [];
28 30 public $attachment = [];
29 31 public $CustomHeader = [];
30 - public $preheader;
31 - public $replyname;
32 - public $replyemail;
33 - public $body;
34 - public $altbody;
35 - public $subject;
36 - public $from;
37 - public $fromName;
38 - public $replyto;
39 - public $custom_view;
40 32
41 - private $encodingHelper;
42 - private $editorHelper;
43 - private $userClass;
44 - private $mailClass;
45 - private $mailArchiveClass;
46 - public $config;
33 + public string $preheader;
34 + public string $replyname;
35 + public string $replyemail;
36 + public string $body;
37 + public string $altbody;
38 + public string $subject;
39 + public string $from;
40 + public string $fromName;
41 + public array $replyto;
42 + public bool $custom_view;
47 43
48 - public $report = true;
49 - public $alreadyCheckedAddresses = false;
50 - public $errorNumber = 0;
51 - public $errorNewTry = [1, 6];
52 - public $failedCounting = true;
53 - public $autoAddUser = false;
54 - public $userCreationTriggers = true;
44 + private EncodingHelper $encodingHelper;
45 + private EditorHelper $editorHelper;
46 + private UserClass $userClass;
47 + private MailClass $mailClass;
48 + private MailArchiveClass $mailArchiveClass;
49 + public ConfigurationClass $config;
50 +
51 + public bool $report = true;
52 + public bool $alreadyCheckedAddresses = false;
53 + public int $errorNumber = 0;
54 + // Can be used when our sending method is temporary unavailable to not count it as a "real" failed
55 + public bool $failedCounting = true;
56 +
57 + // Used by external code
58 + public bool $autoAddUser = false;
59 + public bool $userCreationTriggers = true;
60 +
55 61 public string $reportMessage = '';
56 62 public bool $dtextsFailed = false;
57 63
58 - public $trackEmail = false;
64 + // Should we track the sending of a message (used for welcoming message)
65 + public bool $trackEmail = false;
59 66
60 - public $externalMailer;
67 + // Sending method used in the configuration
68 + public string $externalMailer;
61 69
62 - public $stylesheet = '';
63 - public $settings;
70 + // To import custom stylesheet from user
71 + public string $stylesheet = '';
72 + public array $settings;
64 73
65 - public $parameters = [];
74 + // Used to store special dynamic text content
75 + public array $parameters = [];
66 76
67 - public $userLanguage = '';
68 - public $receiverEmail;
77 + private string $userLanguage = '';
78 + private string $receiverEmail;
69 79
70 - public $overrideEmailToSend = '';
80 + // Special send statuses
81 + private object $overrideEmailToSend;
71 82 public bool $isTest = false;
72 - public $isSpamTest = false;
73 - public $isForward = false;
83 + public bool $isSpamTest = false;
84 + public bool $isForward = false;
74 85
75 - public $clientId = '';
76 - public $clientSecret = '';
77 - public $refreshToken = '';
78 - public $oauthToken = '';
79 - public $expiredIn = '';
80 - public $mailId = null;
81 - public $creator_id;
82 - public $type;
83 - public $links_language;
84 - public $id = null;
85 - public $mail = null;
86 - public $defaultMail = [];
86 + public int $mailId = 0;
87 + public int $creator_id;
88 + public string $type;
89 + public string $links_language;
90 + public int $id = 0;
91 + public ?object $mail = null;
92 + // Mail objects with replaceContent ran
93 + private array $defaultMail = [];
87 94
88 - public $listsIds = [];
89 - private $currentMethodSetting = [];
95 + // Sending method list plugin
96 + public array $listsIds = [];
97 + private string $currentSendingMethod = '';
98 + private array $currentMethodSetting = [];
90 99
91 - public $isAbTest = false;
92 - public $isTransactional = false;
93 - public $isOneTimeMail = false;
94 - private $currentSendingMethod = '';
95 - public $isSendingMethodByListActive = false;
100 + public bool $isAbTest = false;
101 + public bool $isTransactional = false;
102 + public bool $isOneTimeMail = false;
103 + public bool $isSendingMethodByListActive = false;
104 + public string $portError;
96 105
97 106 public function __construct()
98 107 {
99 108 parent::__construct();
100 109
110 + // The DKIM fails when the X-Mailer is added and the user uses their own keys, it makes no sense D:
111 + $this->XMailer = ' ';
112 + $this->SMTPAutoTLS = false;
113 +
101 114 $this->encodingHelper = new EncodingHelper();
102 115 $this->editorHelper = new EditorHelper();
103 116 $this->userClass = new UserClass();
104 117 $this->mailClass = new MailClass();
@@ -103,10 +116,11 @@
103 116 $this->userClass = new UserClass();
104 117 $this->mailClass = new MailClass();
105 118 $this->mailArchiveClass = new MailArchiveClass();
106 119 $this->config = acym_config();
120 +
107 121 $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
108 - $this->Sender = $this->cleanText($this->config->get('bounce_email'));
122 + $this->Sender = $this->cleanText($this->getSendSettings('bounce_email'));
109 123 if (empty($this->Sender)) {
110 124 $this->Sender = '';
111 125 }
112 126 $maxLineLength = $this->config->get('mailer_wordwrap', 0);
@@ -115,11 +129,13 @@
115 129 }
116 130
117 131 $this->setSendingMethodSetting();
118 132
133 + //Do we have a DKIM validation?
119 134 if ($this->config->get('dkim', 0) && $this->Mailer != 'elasticemail') {
120 135 $this->DKIM_domain = $this->config->get('dkim_domain');
121 136 $this->DKIM_selector = $this->config->get('dkim_selector', 'acy');
137 + //Just in case of...
122 138 if (empty($this->DKIM_selector)) $this->DKIM_selector = 'acy';
123 139 $this->DKIM_passphrase = $this->config->get('dkim_passphrase');
124 140 $this->DKIM_identity = $this->config->get('dkim_identity');
125 141 $this->DKIM_private = trim($this->config->get('dkim_private'));
@@ -125,8 +141,9 @@
125 141 $this->DKIM_private = trim($this->config->get('dkim_private'));
126 142 $this->DKIM_private_string = trim($this->config->get('dkim_private'));
127 143 }
128 144
145 + //Set the Charset, by default 'utf-8'
129 146 $this->CharSet = strtolower($this->config->get('charset'));
130 147 if (empty($this->CharSet)) {
131 148 $this->CharSet = 'utf-8';
132 149 }
@@ -132,78 +149,91 @@
132 149 }
133 150
134 151 $this->clearAll();
135 152
153 + //Set the encoding format, should be 8 bit by default.
136 154 $this->Encoding = $this->config->get('encoding_format');
137 155 if (empty($this->Encoding)) {
138 156 $this->Encoding = '8bit';
139 157 }
140 158
159 + // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- This is for big regex, the default value is 100 000.
141 160 @ini_set('pcre.backtrack_limit', 1000000);
142 161
143 162 $this->SMTPOptions = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true]];
144 -
145 - $this->addParamInfo();
146 163 }
147 164
148 - private function getSendingMethodSettings($settingName, $default = ''): string
165 + public function setSendingMethodSetting(): void
149 166 {
150 - if (empty($this->currentMethodSetting)) {
151 - return $this->config->get($settingName, $default);
152 - }
167 + $externalSendingMethod = [];
168 + acym_trigger('onAcymGetSendingMethods', [&$externalSendingMethod, true]);
169 + $externalSendingMethod = !empty($externalSendingMethod['sendingMethods']) ? array_keys($externalSendingMethod['sendingMethods']) : [];
153 170
154 - return $this->currentMethodSetting[$settingName] ?? $default;
155 - }
171 + $mailerMethodConfig = $this->getSendingMethod();
172 + $this->currentSendingMethod = $mailerMethodConfig;
156 173
157 - private function getSendingMethod(): string
158 - {
159 - $this->currentMethodSetting = [];
174 + // Default mailer is to use PHP's mail function
175 + if ($mailerMethodConfig === 'google') {
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';
160 182
161 - $sendingMethods = $this->config->get('sending_method_list', '{}');
162 - $sendingMethods = json_decode($sendingMethods, true);
163 - $sendingMethodsByList = $this->config->get('sending_method_list_by_list', '{}');
164 - $sendingMethodsByList = json_decode($sendingMethodsByList, true);
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 + );
165 198
166 - if (empty($this->listsIds) || empty($sendingMethods) || empty($sendingMethodsByList) || empty($sendingMethodsByList[$this->listsIds[0]])) {
167 - return $this->config->get('mailer_method', 'phpmail');
168 - }
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';
169 211
170 - $sendingMethodIds = [];
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 + );
171 227
172 - foreach ($this->listsIds as $listId) {
173 - if (!empty($sendingMethodsByList[$listId])) {
174 - $sendingMethodIds[] = $sendingMethodsByList[$listId];
228 + if (empty($this->Sender)) {
229 + $this->Sender = strpos($this->Username, '@') ? $this->Username : $this->getSendingMethodSettings('from_email');
175 230 }
176 - }
177 -
178 - if (count($sendingMethodIds) !== count($this->listsIds) || count(array_unique($sendingMethodIds)) !== 1) {
179 - return $this->config->get('mailer_method', 'phpmail');
180 - }
181 -
182 - $sendingMethodId = $sendingMethodIds[0];
183 -
184 - if (empty($sendingMethods[$sendingMethodId])) {
185 - return $this->config->get('mailer_method', 'phpmail');
186 - }
187 -
188 - $this->currentMethodSetting = $sendingMethods[$sendingMethodId];
189 -
190 - return $sendingMethods[$sendingMethodId]['mailer_method'];
191 - }
192 -
193 - public function setSendingMethodSetting(): void
194 - {
195 - $externalSendingMethod = [];
196 - acym_trigger('onAcymGetSendingMethods', [&$externalSendingMethod, true]);
197 - $externalSendingMethod = array_keys($externalSendingMethod['sendingMethods']);
198 -
199 - $mailerMethodConfig = $this->getSendingMethod();
200 - $this->currentSendingMethod = $mailerMethodConfig;
201 -
202 - if ($mailerMethodConfig === 'smtp') {
231 + } elseif ($mailerMethodConfig === 'smtp') {
203 232 $this->isSMTP();
204 233 $this->Host = trim($this->getSendingMethodSettings('smtp_host'));
205 234 $port = $this->getSendingMethodSettings('smtp_port');
235 + // 465 is default port for SSL
206 236 if (empty($port) && $this->getSendingMethodSettings('smtp_secured') === 'ssl') {
207 237 $port = 465;
208 238 }
209 239 if (!empty($port)) {
@@ -208,45 +238,20 @@
208 238 }
209 239 if (!empty($port)) {
210 240 $this->Host .= ':'.$port;
211 241 }
212 - $this->SMTPAuth = (bool)$this->getSendingMethodSettings('smtp_auth', true);
242 + $this->SMTPAuth = (bool)$this->getSendingMethodSettings('smtp_auth', '1');
213 243 $this->Username = trim($this->getSendingMethodSettings('smtp_username'));
214 - $connectionType = $this->getSendingMethodSettings('smtp_type');
215 - $hostName = explode(':', $this->Host)[0];
216 244
217 - if (OAuth::hostRequireOauth($hostName, $connectionType)) {
218 - $this->AuthType = 'XOAUTH2';
219 -
220 - $this->clientSecret = trim($this->getSendingMethodSettings('smtp_secret'));
221 - $this->clientId = trim($this->getSendingMethodSettings('smtp_clientId'));
222 - $this->refreshToken = trim($this->getSendingMethodSettings('smtp_refresh_token'));
223 - $this->oauthToken = trim($this->getSendingMethodSettings('smtp_token'));
224 - $this->expiredIn = trim($this->getSendingMethodSettings('smtp_token_expireIn'));
225 -
226 - $oauth = new OAuth(
227 - [
228 - 'userName' => $this->Username,
229 - 'clientSecret' => $this->clientSecret,
230 - 'oauthToken' => $this->oauthToken,
231 - 'clientId' => $this->clientId,
232 - 'refreshToken' => $this->refreshToken,
233 - 'expiredIn' => $this->expiredIn,
234 - 'host' => $hostName,
235 - ]
236 - );
237 - $this->setOAuth($oauth);
238 - } else {
239 - $authMethod = $this->getSendingMethodSettings('smtp_method');
240 - if (in_array($authMethod, ['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'])) {
241 - $this->AuthType = $authMethod;
242 - }
243 - if ($this->SMTPAuth) {
244 - $this->Password = trim($this->getSendingMethodSettings('smtp_password'));
245 - }
245 + $authMethod = $this->getSendingMethodSettings('smtp_method');
246 + if (in_array($authMethod, ['CRAM-MD5', 'LOGIN', 'PLAIN'])) {
247 + $this->AuthType = $authMethod;
246 248 }
249 + if ($this->SMTPAuth) {
250 + $this->Password = trim($this->getSendingMethodSettings('smtp_password'));
251 + }
247 252
248 - $this->SMTPSecure = trim((string)$this->getSendingMethodSettings('smtp_secured'));
253 + $this->SMTPSecure = trim($this->getSendingMethodSettings('smtp_secured'));
249 254
250 255 if (empty($this->Sender)) {
251 256 $this->Sender = strpos($this->Username, '@') ? $this->Username : $this->getSendingMethodSettings('from_email');
252 257 }
@@ -282,16 +287,13 @@
282 287 $this->isMail();
283 288 }
284 289 }
285 290
286 - public function isExternal($method)
291 + /**
292 + * Dynamically called from PHPMailer
293 + */
294 + protected function externalSend(string $MIMEHeader, string $MIMEBody): bool
287 295 {
288 - $this->Mailer = 'external';
289 - $this->externalMailer = $method;
290 - }
291 -
292 - protected function externalSend($MIMEHeader, $MIMEBody)
293 - {
294 296 $fromName = empty($this->FromName) ? $this->config->get('from_name') : $this->FromName;
295 297 $reply_to = array_shift($this->ReplyTo);
296 298
297 299 $attachments = [];
@@ -324,9 +326,9 @@
324 326
325 327 return true;
326 328 }
327 329
328 - public function send()
330 + public function send(): bool
329 331 {
330 332 if (!file_exists(ACYM_LIBRARIES.'Mailer'.DS.'Mailer.php')) {
331 333 $this->reportMessage = acym_translationSprintf('ACYM_X_FILE_MISSING', 'Mailer', ACYM_LIBRARIES.'Mailer'.DS);
332 334 if ($this->report) {
@@ -349,8 +351,9 @@
349 351 return false;
350 352 }
351 353 }
352 354
355 + //Check if there is at least one reply to otherwise add the default one.
353 356 if (empty($this->ReplyTo) && empty($this->ReplyToQueue)) {
354 357 if (!empty($this->replyemail)) {
355 358 $replyToEmail = $this->replyemail;
356 359 } elseif ($this->config->get('from_as_replyto', 1) == 1) {
@@ -366,11 +369,12 @@
366 369 } else {
367 370 $replyToName = $this->getSendSettings('replyto_name');
368 371 }
369 372
370 - $this->_addReplyTo($replyToEmail, $replyToName);
373 + $this->acymAddReplyTo($replyToEmail, $replyToName);
371 374 }
372 375
376 + // Embed images if there are images to embed
373 377 $shouldEmbed = $this->config->get('embed_images', 0);
374 378 if (intval($shouldEmbed) === 1 && $this->Mailer !== 'elasticemail') {
375 379 $this->embedImages();
376 380 }
@@ -396,8 +400,9 @@
396 400
397 401 return false;
398 402 }
399 403
404 + //Check the from address
400 405 if (empty($this->From) || !acym_isValidEmail($this->From)) {
401 406 $this->reportMessage = acym_translation('ACYM_VALID_EMAIL').' ( '.acym_translation('ACYM_FROM_EMAIL').' : '.$this->From.' ) ';
402 407 $this->errorNumber = 9;
403 408 if ($this->report) {
@@ -421,10 +426,13 @@
421 426 if ($this->config->get('save_body') === '1') {
422 427 @file_put_contents(ACYM_ROOT.'acydebug_mail.html', $this->Body);
423 428 }
424 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!
425 432 $this->Body = htmlentities($this->Body);
426 433 $this->Body = htmlspecialchars_decode($this->Body);
434 + //Fix The Bat issues for special encoding as &sigmaf; was interpreted as &sigma;f;...
427 435 $this->Body = str_replace(['&amp;', '&sigmaf;'], ['&', 'ς'], $this->Body);
428 436
429 437 if ($this->CharSet !== 'utf-8') {
430 438 $this->Body = $this->encodingHelper->change($this->Body, 'UTF-8', $this->CharSet);
@@ -430,8 +438,9 @@
430 438 $this->Body = $this->encodingHelper->change($this->Body, 'UTF-8', $this->CharSet);
431 439 $this->Subject = $this->encodingHelper->change($this->Subject, 'UTF-8', $this->CharSet);
432 440 }
433 441
442 + // These characters can break the send process, let's remove them from the subject
434 443 $this->Subject = str_replace(
435 444 ['’', '“', '”', '–'],
436 445 ["'", '"', '"', '-'],
437 446 $this->Subject
@@ -436,8 +445,9 @@
436 445 ["'", '"', '"', '-'],
437 446 $this->Subject
438 447 );
439 448
449 + // BE CAREFUL! This space is not a space, it's a ALT0160 chr(194) by char(32) which means almost &nbsp;
440 450 $this->Body = str_replace(" ", ' ', $this->Body);
441 451
442 452 $externalSending = false;
443 453
@@ -468,8 +478,9 @@
468 478 }
469 479 }
470 480
471 481
482 + //display error if bloque is displayed... for free.fr especially
472 483 if (!empty($warnings) && strpos($warnings, 'bloque')) {
473 484 $result = false;
474 485 }
475 486
@@ -489,8 +500,9 @@
489 500 $this->reportMessage .= " \n\n ".$warnings;
490 501 }
491 502 $this->errorNumber = 1;
492 503 if ($this->report) {
504 + //We display the report here... we add a link to our doc for the "could not instantiate mail function".
493 505 $this->reportMessage = str_replace(
494 506 'Could not instantiate mail function',
495 507 '<a target="_blank" href="'.ACYM_DOCUMENTATION.'faq/could-not-instantiate-mail-function">'.acym_translation('ACYM_COUND_NOT_INSTANCIATE_MAIL_FUCNTION').'</a>',
496 508 $this->reportMessage
@@ -507,15 +519,15 @@
507 519 );
508 520 } else {
509 521 $this->reportMessage = acym_translationSprintf('ACYM_SEND_SUCCESS', '<b>'.$this->Subject.'</b>', '<b>'.implode(' , ', $receivers).'</b>');
510 522 }
523 +
511 524 if (!empty($warnings)) {
512 525 $this->reportMessage .= " \n\n ".$warnings;
513 526 }
514 - if ($this->report) {
515 - if (acym_isAdmin()) {
516 - acym_enqueueMessage(preg_replace('#(<br( ?/)?>){2}#', '<br />', nl2br($this->reportMessage)), 'info');
517 - }
527 +
528 + if ($this->report && acym_isAdmin()) {
529 + acym_enqueueMessage(preg_replace('#(<br( ?/)?>){2}#', '<br />', nl2br($this->reportMessage)), 'info');
518 530 }
519 531 }
520 532
521 533 return $result;
@@ -520,9 +532,9 @@
520 532
521 533 return $result;
522 534 }
523 535
524 - public function clearAll()
536 + public function clearAll(): void
525 537 {
526 538 $this->Subject = '';
527 539 $this->Body = '';
528 540 $this->AltBody = '';
@@ -535,10 +547,11 @@
535 547 $this->ErrorInfo = '';
536 548 $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
537 549 }
538 550
539 - public function load(int $mailId, $user = null)
551 + public function load(int $mailId, ?object $user = null): ?object
540 552 {
553 + // If it's already loaded return the email
541 554 if (isset($this->defaultMail[$mailId])) {
542 555 return $this->defaultMail[$mailId];
543 556 }
544 557
@@ -552,9 +565,9 @@
552 565 if (!$this->isAbTest && empty($this->overrideEmailToSend) && acym_isMultilingual()) {
553 566 $defaultLanguage = $this->config->get('multilingual_default', ACYM_DEFAULT_LANGUAGE);
554 567 $mails = $this->mailClass->getMultilingualMails($mailId);
555 568
556 - $this->userLanguage = $user != null && !empty($user->language) ? $user->language : $defaultLanguage;
569 + $this->userLanguage = !empty($user->language) ? $user->language : $defaultLanguage;
557 570
558 571 if (!empty($mails)) {
559 572 $languages = array_keys($mails);
560 573 if (count($languages) == 1) {
@@ -570,9 +583,9 @@
570 583 }
571 584 } else {
572 585 unset($this->defaultMail[$mailId]);
573 586
574 - return false;
587 + return null;
575 588 }
576 589
577 590 $acymLanguages['userLanguage'] = $this->userLanguage;
578 591 $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
@@ -577,23 +590,24 @@
577 590 $acymLanguages['userLanguage'] = $this->userLanguage;
578 591 $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
579 592 }
580 593
594 + //We could not load the email...
581 595 if (empty($this->defaultMail[$mailId]->id)) {
582 596 unset($this->defaultMail[$mailId]);
583 597
584 - return false;
598 + return null;
585 599 }
586 600
587 601 if (!empty($this->defaultMail[$mailId]->attachments)) {
588 602 $this->defaultMail[$mailId]->attach = [];
589 603
590 - $attachments = json_decode($this->defaultMail[$mailId]->attachments);
604 + $attachments = json_decode($this->defaultMail[$mailId]->attachments, true);
591 605 foreach ($attachments as $oneAttach) {
592 606 $attach = new \stdClass();
593 - $attach->name = basename($oneAttach->filename);
594 - $attach->filename = str_replace(['/', '\\'], DS, ACYM_ROOT).$oneAttach->filename;
595 - $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'];
596 610 $this->defaultMail[$mailId]->attach[] = $attach;
597 611 }
598 612 }
599 613
@@ -599,8 +613,11 @@
599 613
600 614
601 615 $this->mailId = $mailId;
602 616 $this->id = $this->mailId;
617 + if (empty($this->defaultMail[$mailId]->language)) {
618 + $this->defaultMail[$mailId]->language = acym_getDefaultNewsletterLanguage();
619 + }
603 620 $this->mail = clone $this->defaultMail[$mailId];
604 621 acym_trigger('replaceContent', [&$this->defaultMail[$mailId], true]);
605 622
606 623 $this->prepareEmailContent($mailId);
@@ -610,81 +627,16 @@
610 627
611 628 return $this->defaultMail[$mailId];
612 629 }
613 630
614 - private function storeArchiveVersion($mailId)
615 - {
616 - $mailId = intval($mailId);
617 - if (empty($mailId) || $this->defaultMail[$mailId]->type !== MailClass::TYPE_STANDARD) {
618 - return;
619 - }
620 -
621 - $mailArchive = new \stdClass();
622 -
623 - $alreadyExisting = $this->mailArchiveClass->getOneByMailId($mailId);
624 - if (!empty($alreadyExisting)) {
625 - $mailArchive->id = $alreadyExisting->id;
626 - }
627 -
628 - $mailArchive->mail_id = $mailId;
629 - $mailArchive->date = time();
630 - $mailArchive->body = $this->defaultMail[$mailId]->body;
631 - $mailArchive->subject = $this->defaultMail[$mailId]->subject;
632 - $mailArchive->settings = $this->defaultMail[$mailId]->settings;
633 - $mailArchive->stylesheet = $this->defaultMail[$mailId]->stylesheet;
634 - $mailArchive->attachments = $this->defaultMail[$mailId]->attachments;
635 - $this->mailArchiveClass->save($mailArchive);
636 - }
637 -
638 - private function canTrack(int $mailId, object $user): bool
639 - {
640 - if ($this->isTest || empty($mailId) || empty($user) || !isset($user->tracking) || $user->tracking != 1) {
641 - return false;
642 - }
643 -
644 - $mail = $this->mailClass->getOneById($mailId);
645 - if (!empty($mail) && $mail->tracking != 1) {
646 - return false;
647 - }
648 -
649 - $lists = $this->mailClass->getAllListsByMailIdAndUserId($mailId, $user->id);
650 -
651 - foreach ($lists as $list) {
652 - if ($list->tracking != 1) {
653 - return false;
654 - }
655 - }
656 -
657 - return true;
658 - }
659 -
660 - private function loadUser($user)
661 - {
662 - if (is_string($user) && strpos($user, '@')) {
663 - $receiver = $this->userClass->getOneByEmail($user);
664 -
665 - if (empty($receiver) && $this->autoAddUser && acym_isValidEmail($user)) {
666 - $newUser = new \stdClass();
667 - $newUser->email = $user;
668 - $this->userClass->checkVisitor = false;
669 - $this->userClass->sendConf = false;
670 - acym_setVar('acy_source', 'When sending a test');
671 - $this->userClass->triggers = $this->userCreationTriggers;
672 - $userId = $this->userClass->save($newUser);
673 - $receiver = $this->userClass->getOneById($userId);
674 - }
675 - } elseif (is_object($user)) {
676 - $receiver = $user;
677 - } else {
678 - $receiver = $this->userClass->getOneById($user);
679 - }
680 -
681 - $this->userLanguage = empty($receiver->language) ? acym_getLanguageTag() : $receiver->language;
682 - $this->receiverEmail = $receiver->email;
683 -
684 - return $receiver;
685 - }
686 -
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 + */
687 639 public function sendOne($mailId, $user, array $options = []): bool
688 640 {
689 641 $this->clearAll();
690 642
@@ -693,9 +645,9 @@
693 645 $this->dtextsFailed = false;
694 646
695 647 if (empty($receiver->email)) {
696 648 $this->errorNumber = 4;
697 - $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>');
698 650 if ($this->report) {
699 651 acym_enqueueMessage($this->reportMessage, 'error');
700 652 }
701 653
@@ -701,11 +653,13 @@
701 653
702 654 return false;
703 655 }
704 656
657 + // Can be a string for Joomla 3 and WordPress
705 658 $mailId = intval($mailId);
706 659 $receiver->id = intval($receiver->id);
707 660
661 + // Load the mail with global tags replaced
708 662 if (!$this->load($mailId, $receiver)) {
709 663 $this->reportMessage = 'Can not load the email with ID n°'.$mailId;
710 664 $this->errorNumber = 2;
711 665
@@ -722,16 +676,16 @@
722 676 $this->prepareBcc($mailId, $options);
723 677 $this->prepareAttachments($mailId, $options);
724 678 $this->replaceDynamicContent($mailId);
725 679 $this->prepareTracking($mailId, $receiver);
726 -
727 680 if (!$this->handleDtexts($receiver)) {
728 681 $this->dtextsFailed = true;
729 682
730 683 return false;
731 684 }
732 - $status = (bool)$this->send();
733 685
686 + $status = $this->send();
687 +
734 688 $this->initStatistics($status, $receiver->id);
735 689
736 690 return $status;
737 691 }
@@ -770,273 +724,10 @@
770 724
771 725 $followUpClass->triggerFollowUp(intval($followUp->id), intval($userId), $delay, $mailToSkip);
772 726 }
773 727
774 - private function prepareContent(int $mailId, array $options): void
728 + public function statClick(int $mailId, int $userid, bool $fromStat = false, string $userkey = ''): void
775 729 {
776 - $this->isHTML();
777 -
778 - if (!empty($this->defaultMail[$mailId]->stylesheet)) {
779 - $this->stylesheet = $this->defaultMail[$mailId]->stylesheet;
780 - }
781 - $this->settings = empty($this->defaultMail[$mailId]->settings) ? [] : json_decode($this->defaultMail[$mailId]->settings, true);
782 -
783 - $this->Subject = $this->defaultMail[$mailId]->subject;
784 - $this->Body = $this->defaultMail[$mailId]->body;
785 - if ($this->isTest && !empty($options['testNote'])) {
786 - $this->Body = '<div style="text-align: center; padding: 25px; font-family: Poppins; font-size: 20px">'.$options['testNote'].'</div>'.$this->Body;
787 - }
788 -
789 - if (!empty($this->introtext)) {
790 - $this->Body = $this->introtext.$this->Body;
791 - }
792 -
793 - if (!empty($this->defaultMail[$mailId]->preheader)) {
794 - $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;">';
795 - $preHeader .= $this->defaultMail[$mailId]->preheader.str_repeat('&nbsp;&zwnj;', 100);
796 - $preHeader .= '</div><!--<![endif]-->';
797 -
798 - preg_match('#(<(.*)<body(.*)>)#Uis', $this->Body, $matches);
799 - if (empty($matches) || empty($matches[1])) {
800 - $this->Body = $preHeader.$this->Body;
801 - } else {
802 - $this->Body = $matches[1].$preHeader.str_replace($matches[1], '', $this->Body);
803 - }
804 - }
805 -
806 - if (ACYM_CMS === 'wordpress') {
807 - ob_start();
808 - $this->Body = do_shortcode($this->Body);
809 - ob_end_clean();
810 - }
811 - }
812 -
813 - private function prepareHeaders(int $mailId, int $receiverId, array $options): void
814 - {
815 - $subject = base64_encode(rand(0, 9999999)).'AC'.$receiverId.'Y'.$this->defaultMail[$mailId]->id.'BA'.base64_encode(time().rand(0, 99999));
816 - $this->MessageID = '<'.preg_replace('|[^a-z0-9+_]|i', '', $subject).'@'.$this->serverHostname().'>';
817 - $this->addCustomHeader('Feedback-ID', $this->defaultMail[$mailId]->id.':'.$receiverId.':'.$this->defaultMail[$mailId]->type.':'.base64_encode(ACYM_ROOT));
818 -
819 - if (!empty($this->defaultMail[$mailId]->headers)) {
820 - $this->mailHeader = $this->defaultMail[$mailId]->headers;
821 - }
822 -
823 - if (!empty($options['headers'])) {
824 - foreach ($options['headers'] as $headerName => $headerValue) {
825 - $this->addCustomHeader(trim($headerName), trim($headerValue));
826 - }
827 - }
828 - }
829 -
830 - private function prepareAddresses(int $mailId, object $receiver): void
831 - {
832 - $receiverName = '';
833 - if ($this->config->get('add_names', true)) {
834 - $receiverName = $this->cleanText($receiver->name);
835 - if ($receiverName == $this->cleanText($receiver->email)) {
836 - $receiverName = '';
837 - }
838 - }
839 - $this->addAddress($this->cleanText($receiver->email), $receiverName);
840 -
841 - $this->setFrom($this->getSendSettings('from_email', $mailId), $this->getSendSettings('from_name', $mailId));
842 - $this->_addReplyTo($this->defaultMail[$mailId]->reply_to_email, $this->defaultMail[$mailId]->reply_to_name);
843 -
844 - if (!empty($this->defaultMail[$mailId]->bounce_email)) {
845 - $this->Sender = $this->cleanText($this->defaultMail[$mailId]->bounce_email);
846 - } else {
847 - $this->Sender = $this->cleanText($this->config->get('bounce_email'));
848 - }
849 - }
850 -
851 - private function prepareBcc(int $mailId, array $options): void
852 - {
853 - $bccAddresses = [];
854 -
855 - if (!empty($options['bcc'])) {
856 - $bccAddresses = $options['bcc'];
857 - }
858 -
859 - if (!empty($this->defaultMail[$mailId]->bcc)) {
860 - $emailBccAddresses = explode(';', trim(str_replace([',', ' '], ';', $this->defaultMail[$mailId]->bcc)));
861 - $bccAddresses = array_unique(array_merge($bccAddresses, $emailBccAddresses));
862 - }
863 -
864 - if (!empty($bccAddresses)) {
865 - $bccAddresses = array_filter($bccAddresses);
866 - foreach ($bccAddresses as $oneBccAddress) {
867 - $this->loadUser($oneBccAddress);
868 - $this->addBCC($oneBccAddress);
869 - }
870 - }
871 - }
872 -
873 - private function prepareAttachments(int $mailId, array $options): void
874 - {
875 - $attachments = [];
876 -
877 - if (!empty($this->defaultMail[$mailId]->attach)) {
878 - $attachments = $this->defaultMail[$mailId]->attach;
879 - }
880 -
881 - if (!empty($options['attachments'])) {
882 - $attachments = array_merge($attachments, $options['attachments']);
883 - }
884 -
885 - if (empty($attachments)) {
886 - return;
887 - }
888 -
889 - if ($this->config->get('embed_files')) {
890 - foreach ($attachments as $attachment) {
891 - $this->addAttachment($attachment->filename, $attachment->name);
892 - }
893 - } else {
894 - $attachStringHTML = '<fieldset><legend>'.acym_translation('ACYM_ATTACHMENTS').'</legend><table>';
895 - foreach ($attachments as $attachment) {
896 - $attachStringHTML .= '<tr><td><a href="'.$attachment->url.'" target="_blank">'.$attachment->name.'</a></td></tr>';
897 - }
898 - $attachStringHTML .= '</table></fieldset>';
899 -
900 - if ($this->config->get('attachments_position', 'bottom') === 'top') {
901 - $this->Body = $attachStringHTML.'<br />'.$this->Body;
902 - } else {
903 - $this->Body .= '<br />'.$attachStringHTML;
904 - }
905 - }
906 - }
907 -
908 - private function replaceDynamicContent(int $mailId): void
909 - {
910 - $this->replaceParams();
911 -
912 - $this->body = &$this->Body;
913 - $this->altbody = &$this->AltBody;
914 - $this->subject = &$this->Subject;
915 - $this->from = &$this->From;
916 - $this->fromName = &$this->FromName;
917 - $this->replyto = &$this->ReplyTo;
918 - $this->replyname = $this->defaultMail[$mailId]->reply_to_name;
919 - $this->replyemail = $this->defaultMail[$mailId]->reply_to_email;
920 - $this->mailId = $this->defaultMail[$mailId]->id;
921 - $this->id = $this->mailId;
922 - $this->creator_id = $this->defaultMail[$mailId]->creator_id;
923 - $this->type = $this->defaultMail[$mailId]->type;
924 - $this->stylesheet = &$this->stylesheet;
925 - $this->links_language = $this->defaultMail[$mailId]->links_language;
926 - }
927 -
928 - private function prepareTracking(int $mailId, object $receiver): void
929 - {
930 - if ($this->canTrack($mailId, $receiver)) {
931 - $this->statPicture($this->mailId, $receiver->id);
932 - $this->body = acym_absoluteURL($this->body);
933 - $this->statClick($this->mailId, $receiver->id);
934 - if (acym_isTrackingSalesActive()) {
935 - $this->trackingSales($this->mailId, $receiver->id);
936 - }
937 - }
938 - }
939 -
940 - private function handleDtexts(object $receiver): bool
941 - {
942 - $this->replaceParams();
943 -
944 - if (strpos($receiver->email, '@mt.acyba.com') !== false) {
945 - $currentUser = $this->userClass->getOneByEmail(acym_currentUserEmail());
946 - if (empty($currentUser)) {
947 - $currentUser = $receiver;
948 - }
949 - $result = acym_trigger('replaceUserInformation', [&$this, &$currentUser, true]);
950 - } else {
951 - $result = acym_trigger('replaceUserInformation', [&$this, &$receiver, true]);
952 - if (!empty($result)) {
953 - foreach ($result as $oneResult) {
954 - if (!empty($oneResult) && !$oneResult['send']) {
955 - $this->reportMessage = $oneResult['message'];
956 -
957 - return false;
958 - }
959 - }
960 - }
961 - }
962 -
963 - global $acymLanguages;
964 - if (!empty($acymLanguages['userLanguage'])) {
965 - unset($acymLanguages['userLanguage']);
966 - }
967 -
968 - foreach ($result as $oneResult) {
969 - if (!empty($oneResult['emogrifier'])) {
970 - $this->prepareEmailContent();
971 - break;
972 - }
973 - }
974 -
975 - if ($this->config->get('multiple_part', false)) {
976 - $this->altbody = $this->textVersion($this->Body);
977 - }
978 -
979 - $this->replaceParams();
980 -
981 - return true;
982 - }
983 -
984 - private function initStatistics(bool $status, int $receiverId): void
985 - {
986 - if ($this->trackEmail) {
987 - $statsAdd = [];
988 - $statsAdd[$this->mailId][intval($status)][] = $receiverId;
989 -
990 - $queueHelper = new QueueHelper();
991 - $queueHelper->statsAdd($statsAdd);
992 - $this->trackEmail = false;
993 - }
994 - }
995 -
996 - private function trackingSales($mailId, $userId)
997 - {
998 - preg_match_all('#href[ ]*=[ ]*"(?!mailto:|\#|ymsgr:|callto:|file:|ftp:|webcal:|skype:|tel:)([^"]+)"#Ui', $this->body, $results);
999 - if (empty($results)) return;
1000 -
1001 - foreach ($results[1] as $key => $url) {
1002 - $simplifiedUrl = str_replace(['https://', 'http://', 'www.'], '', $url);
1003 - $simplifiedWebsite = str_replace(['https://', 'http://', 'www.'], '', ACYM_LIVE);
1004 - if (strpos($simplifiedUrl, rtrim($simplifiedWebsite, '/')) === false || strpos($url, 'task=unsub')) continue;
1005 -
1006 - $toAddUrl = (strpos($url, '?') === false ? '?' : '&').'linkReferal='.$mailId.'-'.$userId;
1007 -
1008 - $posHash = strpos($url, '#');
1009 - if ($posHash !== false) {
1010 - $newURL = substr($url, 0, $posHash).$toAddUrl.substr($url, $posHash);
1011 - } else {
1012 - $newURL = $url.$toAddUrl;
1013 - }
1014 -
1015 - $this->body = preg_replace('#href="('.preg_quote($url, '#').')"#Uis', 'href="'.$newURL.'"', $this->body);
1016 - }
1017 - }
1018 -
1019 - public function statPicture($mailId, $userId)
1020 - {
1021 - $pictureLink = acym_frontendLink('frontstats&task=openStats&id='.$mailId.'&userid='.$userId, true, false);
1022 -
1023 - $widthsize = 50;
1024 - $heightsize = 1;
1025 - $width = empty($widthsize) ? '' : ' width="'.$widthsize.'" ';
1026 - $height = empty($heightsize) ? '' : ' height="'.$heightsize.'" ';
1027 -
1028 - $statPicture = '<img class="spict" alt="Statistics image" src="'.$pictureLink.'" border="0" '.$height.$width.'/>';
1029 -
1030 - if (strpos($this->body, '</body>')) {
1031 - $this->body = str_replace('</body>', $statPicture.'</body>', $this->body);
1032 - } else {
1033 - $this->body .= $statPicture;
1034 - }
1035 - }
1036 -
1037 - public function statClick($mailId, $userid, $fromStat = false)
1038 - {
1039 730 if (!$fromStat && !in_array($this->type, MailClass::TYPES_WITH_STATS)) {
1040 731 return;
1041 732 }
1042 733
@@ -1056,9 +747,9 @@
1056 747
1057 748 $utmCampaign = acym_getAlias($mail->subject);
1058 749 }
1059 750
1060 - 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);
1061 752 if (empty($results)) {
1062 753 return;
1063 754 }
1064 755
@@ -1069,8 +760,9 @@
1069 760 if ($countLinks[$url] === 1) {
1070 761 continue;
1071 762 }
1072 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
1073 765 $previousIsOutlook = false;
1074 766 if (strpos($results[0][$key], '<v:roundrect') === 0) {
1075 767 $previousLinkHandled = $results[0][$key];
1076 768 if ($countLinks[$url] === 2) {
@@ -1108,8 +800,9 @@
1108 800
1109 801 foreach ($results[1] as $i => $url) {
1110 802 $urlsNotToTrack = [
1111 803 'task=unsub',
804 + 'task=disableTracking',
1112 805 'fonts.googleapis.com',
1113 806 ];
1114 807
1115 808 $track = true;
@@ -1120,22 +813,30 @@
1120 813 break;
1121 814 }
1122 815 }
1123 816
817 + //We don't track unsubscribe link
1124 818 if (isset($urls[$results[0][$i]]) || !$track) {
1125 819 continue;
1126 820 }
1127 821
822 + //We often need to check if the url is within the website... but we don't care about http or https
1128 823 $simplifiedUrl = str_replace(['https://', 'http://', 'www.'], '', $url);
1129 824 $simplifiedWebsite = str_replace(['https://', 'http://', 'www.'], '', ACYM_LIVE);
1130 825 $internalUrl = strpos($simplifiedUrl, rtrim($simplifiedWebsite, '/')) === 0;
1131 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
1132 829 $subfolder = false;
1133 830 if ($internalUrl) {
1134 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
1135 834 if (strpos($urlWithoutBase, '/') || strpos($urlWithoutBase, '?')) {
835 + // Get the supposed sub-folder name
1136 836 $slashPosition = strpos($urlWithoutBase, '/');
1137 837 $folderName = substr($urlWithoutBase, 0, !$slashPosition ? strpos($urlWithoutBase, '?') : $slashPosition);
838 + //There is no dot in a folder!
1138 839 if (strpos($folderName, '.') === false) {
1139 840 $subfolder = @is_dir(ACYM_ROOT.$folderName);
1140 841 }
1141 842 }
@@ -1150,8 +851,9 @@
1150 851 $args = [];
1151 852 $args[] = empty($campaign->sending_params['utm_source']) ? 'utm_source=newsletter_'.$idToUse : 'utm_source='.urlencode($campaign->sending_params['utm_source']);
1152 853 $args[] = empty($campaign->sending_params['utm_medium']) ? 'utm_medium=email' : 'utm_medium='.urlencode($campaign->sending_params['utm_medium']);
1153 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
1154 856 $anchor = '';
1155 857 if (strpos($url, '#') !== false) {
1156 858 $anchor = substr($url, strpos($url, '#'));
1157 859 $url = substr($url, 0, strpos($url, '#'));
@@ -1161,40 +863,30 @@
1161 863 $mytracker = $url.'&'.implode('&', $args);
1162 864 } else {
1163 865 $mytracker = $url.'?'.implode('&', $args);
1164 866 }
867 + //We add back the anchor if we had one
1165 868 $mytracker .= $anchor;
1166 869 $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
1167 870
871 + //Set the url variable so that we can use it later on...
1168 872 $url = $mytracker;
1169 873 }
1170 874
1171 875 if (strpos($trackingSystem, 'acymailing') !== false) {
1172 - $isAutologin = false;
1173 - $autologinParams = 'autoSubId=%7Bsubscriber:id%7D&amp;subKey=%7Bsubscriber:key%7Curlencode%7D';
1174 - if (strpos($url, $autologinParams) !== false) {
1175 - $isAutologin = true;
1176 - $url = str_replace(
1177 - [
1178 - '?'.$autologinParams.'&amp;',
1179 - '?'.$autologinParams,
1180 - '&amp;'.$autologinParams,
1181 - ],
1182 - [
1183 - '?',
1184 - '',
1185 - '',
1186 - ],
1187 - $url
1188 - );
876 + $isAutologin = strpos($url, self::AUTOLOGIN_PARAMS) !== false;
877 + if ($isAutologin) {
878 + $url = $this->stripAutologinParams($url);
1189 879 }
1190 880
1191 - if (preg_match('#passw|modify|\{|%7B#i', $url)) {
881 + if (preg_match('#passw|modify#i', $url)) {
1192 882 continue;
1193 883 }
1194 884
885 + $urlForRegistration = $url;
886 +
1195 887 if (!$fromStat) {
1196 - $mytracker = $urlClass->getUrl($url, $mailId, $userid);
888 + $mytracker = $urlClass->getUrl($urlForRegistration, $mailId, $userid, $userkey);
1197 889 }
1198 890
1199 891 if (empty($mytracker)) {
1200 892 continue;
@@ -1200,10 +892,10 @@
1200 892 continue;
1201 893 }
1202 894
1203 895 if ($isAutologin) {
1204 - $mytracker .= strpos($mytracker, '?') === false ? '?' : '&amp;';
1205 - $mytracker .= $autologinParams;
896 + $separator = strpos($mytracker, '?') === false ? '?' : '&amp;';
897 + $mytracker .= $separator.self::AUTOLOGIN_PARAMS;
1206 898 }
1207 899
1208 900 $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
1209 901 }
@@ -1211,14 +903,19 @@
1211 903
1212 904 $this->body = str_replace(array_keys($urls), $urls, $this->body);
1213 905 }
1214 906
1215 - public function textVersion($html, $fullConvert = true)
907 + public function textVersion(string $html, bool $fullConvert = true): string
1216 908 {
909 + //Replace relative links into absolute before replacing the text version so that we keep correct urls
1217 910 $html = acym_absoluteURL($html);
1218 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
1219 914 if ($fullConvert) {
915 + //As in HTML multiple spaces are interpreted as only one space, we remove the multiple spaces for the text version
1220 916 $html = preg_replace('# +#', ' ', $html);
917 + //Same thing, return chars don't exist in html so we can simply remove them, neither \t
1221 918 $html = str_replace(["\n", "\r", "\t"], '', $html);
1222 919 }
1223 920
1224 921 $removepictureslinks = "#< *a[^>]*> *< *img[^>]*> *< *\/ *a *>#isU";
@@ -1246,15 +943,20 @@
1246 943 ['', '', '', '', "\n\n", "\n* ", "\n", "\n", '${2} ( ${1} )'],
1247 944 $html
1248 945 );
1249 946
947 + //The striptags function may not do the job properly in some cases...
1250 948 $text = preg_replace('#(&lt;|&\#60;)([^ \n\r\t])#i', '&lt; ${2}', $text);
1251 949
1252 - $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 :) :) :)
1253 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.
1254 955 $text = trim(@html_entity_decode($text, ENT_QUOTES, 'UTF-8'));
1255 956
1256 957 if ($fullConvert) {
958 + //We do that one more time as some extra spaces may have appeared
1257 959 $text = preg_replace('# +#', ' ', $text);
1258 960 $text = preg_replace('#\n *\n\s+#', "\n\n", $text);
1259 961 }
1260 962
@@ -1260,9 +962,12 @@
1260 962
1261 963 return $text;
1262 964 }
1263 965
1264 - protected function embedImages()
966 + /**
967 + * @throws Exception
968 + */
969 + protected function embedImages(): bool
1265 970 {
1266 971 preg_match_all('/(src|background)=[\'|"]([^"\']*)[\'|"]/Ui', $this->Body, $images);
1267 972
1268 973 if (empty($images[2])) {
@@ -1284,12 +989,15 @@
1284 989
1285 990 $allImages = [];
1286 991
1287 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
1288 995 if (isset($allImages[$url])) {
1289 996 continue;
1290 997 }
1291 998
999 + // Don't embed images with a controller, most likely the stats picture
1292 1000 if (strpos($url, 'ctrl=') !== false) {
1293 1001 continue;
1294 1002 }
1295 1003
@@ -1294,25 +1002,30 @@
1294 1002 }
1295 1003
1296 1004 $allImages[$url] = 1;
1297 1005
1006 + // We convert the url into local directory
1298 1007 $path = acym_internalUrlToPath($url);
1299 1008 $path = $this->removeAdditionalParams($path);
1300 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 +
1301 1021 $filename = str_replace(['%', ' '], '_', basename($url));
1302 1022 $filename = $this->removeAdditionalParams($filename);
1303 1023
1304 1024 $md5 = md5($filename);
1305 1025 $cid = 'cid:'.$md5;
1306 - $fileParts = explode(".", $filename);
1307 - if (empty($fileParts[1])) {
1308 - continue;
1309 - }
1310 - $ext = strtolower($fileParts[1]);
1311 - if (!isset($mimetypes[$ext])) {
1312 - continue;
1313 - }
1314 1026
1027 + // We only change the url if we were able to embed the image.
1315 1028 if ($this->addEmbeddedImage($path, $md5, $filename, 'base64', $mimetypes[$ext])) {
1316 1029 $this->Body = preg_replace('/'.preg_quote($images[0][$i], '/').'/Ui', $images[1][$i].'="'.$cid.'"', $this->Body);
1317 1030 } else {
1318 1031 $embedSuccess = false;
@@ -1321,139 +1034,580 @@
1321 1034
1322 1035 return $embedSuccess;
1323 1036 }
1324 1037
1325 - private function removeAdditionalParams($url)
1038 + private function getEmbeddableImagePath(string $path): string
1326 1039 {
1327 - $additionalParamsPos = strpos($url, '?');
1328 - if (!empty($additionalParamsPos)) {
1329 - $url = substr($url, 0, $additionalParamsPos);
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 '';
1330 1043 }
1331 1044
1332 - return $url;
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 '';
1049 + }
1050 +
1051 + if (!is_file($path) || !is_readable($path)) {
1052 + return '';
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;
1333 1062 }
1334 1063
1335 - public function cleanText($text)
1064 + public function cleanText(?string $text): string
1336 1065 {
1337 1066 return trim(preg_replace('/(%0A|%0D|\n+|\r+)/i', '', (string)$text));
1338 1067 }
1339 1068
1340 - protected function _addReplyTo($email, $name)
1069 + /**
1070 + * Adds shortcodes available in the email content
1071 + */
1072 + public function addParam(string $name, $value): void
1341 1073 {
1342 - if (empty($email)) {
1074 + $tagName = '{'.$name.'}';
1075 + $this->parameters[$tagName] = $value;
1076 + }
1077 +
1078 + public function overrideEmail(array $options): bool
1079 + {
1080 + $overrideAllEmails = $this->config->get('send_website_emails', 0) == 1;
1081 +
1082 + $overrideClass = new OverrideClass();
1083 + $override = $overrideClass->getMailByBaseContent($options['subject'], $options['message']);
1084 +
1085 + // We let the CMS send the email
1086 + if (empty($override) && !$overrideAllEmails) {
1087 + return false;
1088 + }
1089 +
1090 + // AcyMailing needs to send the email
1091 + $this->autoAddUser = true;
1092 +
1093 + if (empty($override)) {
1094 + $this->trackEmail = false;
1095 +
1096 + return $this->overrideAllEmails($options);
1097 + } else {
1098 + $this->trackEmail = true;
1099 +
1100 + return $this->sendOverride($override, $options);
1101 + }
1102 + }
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 +
1129 + private function getSendingMethodSettings(string $settingName, string $default = ''): string
1130 + {
1131 + if (empty($this->currentMethodSetting)) {
1132 + return $this->config->get($settingName, $default);
1133 + }
1134 +
1135 + return $this->currentMethodSetting[$settingName] ?? $default;
1136 + }
1137 +
1138 + private function getSendingMethod(): string
1139 + {
1140 + $this->currentMethodSetting = [];
1141 +
1142 + $sendingMethods = $this->config->get('sending_method_list', '{}');
1143 + $sendingMethods = json_decode($sendingMethods, true);
1144 + $sendingMethodsByList = $this->config->get('sending_method_list_by_list', '{}');
1145 + $sendingMethodsByList = json_decode($sendingMethodsByList, true);
1146 +
1147 + if (empty($this->listsIds) || empty($sendingMethods) || empty($sendingMethodsByList) || empty($sendingMethodsByList[$this->listsIds[0]])) {
1148 + return $this->config->get('mailer_method', 'phpmail');
1149 + }
1150 +
1151 + $sendingMethodIds = [];
1152 +
1153 + foreach ($this->listsIds as $listId) {
1154 + if (!empty($sendingMethodsByList[$listId])) {
1155 + $sendingMethodIds[] = $sendingMethodsByList[$listId];
1156 + }
1157 + }
1158 +
1159 + if (count($sendingMethodIds) !== count($this->listsIds) || count(array_unique($sendingMethodIds)) !== 1) {
1160 + return $this->config->get('mailer_method', 'phpmail');
1161 + }
1162 +
1163 + $sendingMethodId = $sendingMethodIds[0];
1164 +
1165 + if (empty($sendingMethods[$sendingMethodId])) {
1166 + return $this->config->get('mailer_method', 'phpmail');
1167 + }
1168 +
1169 + $this->currentMethodSetting = $sendingMethods[$sendingMethodId];
1170 +
1171 + return $sendingMethods[$sendingMethodId]['mailer_method'];
1172 + }
1173 +
1174 + private function isExternal(string $method): void
1175 + {
1176 + $this->Mailer = 'external';
1177 + $this->externalMailer = $method;
1178 + }
1179 +
1180 + private function storeArchiveVersion(int $mailId): void
1181 + {
1182 + if ($this->defaultMail[$mailId]->type !== MailClass::TYPE_STANDARD) {
1343 1183 return;
1344 1184 }
1345 - $replyToName = $this->config->get('add_names', true) ? $this->cleanText(trim($name)) : '';
1346 - $replyToEmail = trim($email);
1347 - if (substr_count($replyToEmail, '@') > 1) {
1348 - $replyToEmailArray = explode(';', str_replace([';', ','], ';', $replyToEmail));
1349 - $replyToNameArray = explode(';', str_replace([';', ','], ';', $replyToName));
1350 - foreach ($replyToEmailArray as $i => $oneReplyTo) {
1351 - $this->addReplyTo($this->cleanText($oneReplyTo), @$replyToNameArray[$i]);
1185 +
1186 + $mailArchive = new \stdClass();
1187 +
1188 + $alreadyExisting = $this->mailArchiveClass->getOneByMailId($mailId);
1189 + if (!empty($alreadyExisting)) {
1190 + $mailArchive->id = $alreadyExisting->id;
1191 + }
1192 +
1193 + $mailArchive->mail_id = $mailId;
1194 + $mailArchive->date = time();
1195 + $mailArchive->body = $this->defaultMail[$mailId]->body;
1196 + $mailArchive->subject = $this->defaultMail[$mailId]->subject;
1197 + $mailArchive->settings = $this->defaultMail[$mailId]->settings;
1198 + $mailArchive->stylesheet = $this->defaultMail[$mailId]->stylesheet;
1199 + $mailArchive->attachments = $this->defaultMail[$mailId]->attachments;
1200 + $this->mailArchiveClass->save($mailArchive);
1201 + }
1202 +
1203 + private function canTrack(int $mailId, object $user): bool
1204 + {
1205 + if ($this->isTest || empty($mailId) || empty($user) || !isset($user->tracking) || $user->tracking != 1) {
1206 + return false;
1207 + }
1208 +
1209 + $mail = $this->mailClass->getOneById($mailId);
1210 + if (!empty($mail) && $mail->tracking != 1) {
1211 + return false;
1212 + }
1213 +
1214 + $lists = $this->mailClass->getAllListsByMailIdAndUserId($mailId, $user->id);
1215 +
1216 + foreach ($lists as $list) {
1217 + if ($list->tracking != 1) {
1218 + return false;
1352 1219 }
1220 + }
1221 +
1222 + return true;
1223 + }
1224 +
1225 + /**
1226 + * @param mixed $user
1227 + *
1228 + * @throws Exception
1229 + */
1230 + private function loadUser($user, bool $force = false): object
1231 + {
1232 + if (is_numeric($user)) {
1233 + $receiver = $this->userClass->getOneById($user);
1234 + } elseif (is_object($user)) {
1235 + $receiver = $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);
1240 + }
1241 +
1242 + if (empty($receiver)) {
1243 + throw new Exception(esc_attr(acym_translation('ACYM_USER_NOT_FOUND')));
1244 + }
1245 +
1246 + $this->userLanguage = empty($receiver->language) ? acym_getDefaultNewsletterLanguage() : $receiver->language;
1247 + $this->receiverEmail = (string)($receiver->email ?? '');
1248 +
1249 + return $receiver;
1250 + }
1251 +
1252 + private function prepareContent(int $mailId, array $options): void
1253 + {
1254 + $this->isHTML();
1255 +
1256 + if (!empty($this->defaultMail[$mailId]->stylesheet)) {
1257 + $this->stylesheet = $this->defaultMail[$mailId]->stylesheet;
1258 + }
1259 + $this->settings = empty($this->defaultMail[$mailId]->settings) ? [] : json_decode($this->defaultMail[$mailId]->settings, true);
1260 +
1261 + $this->Subject = $this->defaultMail[$mailId]->subject;
1262 + $this->Body = $this->defaultMail[$mailId]->body;
1263 + if ($this->isTest && !empty($options['testNote'])) {
1264 + $this->Body = '<div style="text-align: center; padding: 25px; font-family: Poppins; font-size: 20px">'.$options['testNote'].'</div>'.$this->Body;
1265 + }
1266 +
1267 + //We add the intro text at the top of the body
1268 + if (!empty($this->introtext)) {
1269 + $this->Body = $this->introtext.$this->Body;
1270 + }
1271 +
1272 + if (!empty($this->defaultMail[$mailId]->preheader)) {
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;
1275 + $preHeader .= '</div><!--<![endif]-->';
1276 +
1277 + //We want to insert the preview at the start of the body so we match the start of the mail
1278 + preg_match('#(<(.*)<body(.*)>)#Uis', $this->Body, $matches);
1279 + if (empty($matches) || empty($matches[1])) {
1280 + $this->Body = $preHeader.$this->Body;
1281 + } else {
1282 + $this->Body = $matches[1].$preHeader.str_replace($matches[1], '', $this->Body);
1283 + }
1284 + }
1285 +
1286 + if (ACYM_CMS === 'wordpress') {
1287 + ob_start();
1288 + $this->Body = do_shortcode($this->Body);
1289 + ob_end_clean();
1290 + }
1291 + }
1292 +
1293 + private function prepareHeaders(int $mailId, int $receiverId, array $options): void
1294 + {
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));
1297 + $this->MessageID = '<'.preg_replace('|[^a-z0-9+_]|i', '', $subject).'@'.$this->serverHostname().'>';
1298 + $this->addCustomHeader('Feedback-ID', $this->defaultMail[$mailId]->id.':'.$receiverId.':'.$this->defaultMail[$mailId]->type.':'.base64_encode(ACYM_ROOT));
1299 +
1300 + if (!empty($this->defaultMail[$mailId]->headers)) {
1301 + $this->mailHeader = $this->defaultMail[$mailId]->headers;
1302 + }
1303 +
1304 + if (!empty($options['headers'])) {
1305 + foreach ($options['headers'] as $headerName => $headerValue) {
1306 + $this->addCustomHeader(trim($headerName), trim($headerValue));
1307 + }
1308 + }
1309 + }
1310 +
1311 + private function prepareAddresses(int $mailId, object $receiver): void
1312 + {
1313 + $receiverName = '';
1314 + if ($this->config->get('add_names', true)) {
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
1317 + if ($receiverName == $this->cleanText($receiver->email)) {
1318 + $receiverName = '';
1319 + }
1320 + }
1321 + $this->addAddress($this->cleanText($receiver->email), $receiverName);
1322 +
1323 + $this->setFrom($this->getSendSettings('from_email', $mailId), $this->getSendSettings('from_name', $mailId));
1324 + $this->acymAddReplyTo($this->defaultMail[$mailId]->reply_to_email ?? '', $this->defaultMail[$mailId]->reply_to_name ?? '');
1325 +
1326 + // This is the bounce email address
1327 + if (!empty($this->defaultMail[$mailId]->bounce_email)) {
1328 + $this->Sender = $this->cleanText($this->defaultMail[$mailId]->bounce_email);
1353 1329 } else {
1354 - $this->addReplyTo($this->cleanText($replyToEmail), $replyToName);
1330 + $this->Sender = $this->cleanText($this->config->get('bounce_email'));
1355 1331 }
1356 1332 }
1357 1333
1358 - private function replaceParams()
1334 + /**
1335 + * @throws Exception
1336 + */
1337 + private function prepareBcc(int $mailId, array $options): void
1359 1338 {
1360 - if (empty($this->parameters)) return;
1339 + $bccAddresses = [];
1361 1340
1362 - $helperPlugin = new PluginHelper();
1341 + if (!empty($options['bcc'])) {
1342 + $bccAddresses = $options['bcc'];
1343 + }
1363 1344
1364 - $this->generateAllParams();
1345 + if (!empty($this->defaultMail[$mailId]->bcc)) {
1346 + $emailBccAddresses = explode(';', trim(str_replace([',', ' '], ';', $this->defaultMail[$mailId]->bcc)));
1347 + $bccAddresses = array_unique(array_merge($bccAddresses, $emailBccAddresses));
1348 + }
1365 1349
1366 - $vars = [
1367 - 'Subject',
1368 - 'Body',
1369 - 'From',
1370 - 'FromName',
1371 - 'replyname',
1372 - 'replyemail',
1373 - ];
1350 + if (!empty($bccAddresses)) {
1351 + $bccAddresses = array_filter($bccAddresses);
1374 1352
1375 - foreach ($vars as $oneVar) {
1376 - if (!empty($this->$oneVar)) {
1377 - $this->$oneVar = $helperPlugin->replaceDText($this->$oneVar, $this->parameters);
1353 + // loadUser() overwrites the current receiver, keep the real one
1354 + $receiverEmail = $this->receiverEmail;
1355 + $userLanguage = $this->userLanguage;
1356 +
1357 + foreach ($bccAddresses as $oneBccAddress) {
1358 + $this->loadUser($oneBccAddress, true);
1359 + $this->addBCC($oneBccAddress);
1378 1360 }
1361 +
1362 + $this->receiverEmail = $receiverEmail;
1363 + $this->userLanguage = $userLanguage;
1379 1364 }
1365 + }
1380 1366
1381 - if (!empty($this->ReplyTo)) {
1382 - foreach ($this->ReplyTo as $i => $replyto) {
1383 - foreach ($replyto as $a => $oneval) {
1384 - $this->ReplyTo[$i][$a] = $helperPlugin->replaceDText($this->ReplyTo[$i][$a], $this->parameters);
1385 - }
1367 + private function prepareAttachments(int $mailId, array $options): void
1368 + {
1369 + $attachments = [];
1370 +
1371 + if (!empty($this->defaultMail[$mailId]->attach)) {
1372 + $attachments = $this->defaultMail[$mailId]->attach;
1373 + }
1374 +
1375 + if (!empty($options['attachments'])) {
1376 + $attachments = array_merge($attachments, $options['attachments']);
1377 + }
1378 +
1379 + if (empty($attachments)) {
1380 + return;
1381 + }
1382 +
1383 + if ($this->config->get('embed_files')) {
1384 + foreach ($attachments as $attachment) {
1385 + $this->addAttachment($attachment->filename, $attachment->name);
1386 1386 }
1387 + } else {
1388 + $attachStringHTML = '<fieldset><legend>'.acym_translation('ACYM_ATTACHMENTS').'</legend><table>';
1389 + foreach ($attachments as $attachment) {
1390 + $attachStringHTML .= '<tr><td><a href="'.$attachment->url.'" target="_blank">'.$attachment->name.'</a></td></tr>';
1391 + }
1392 + $attachStringHTML .= '</table></fieldset>';
1393 +
1394 + if ($this->config->get('attachments_position', 'bottom') === 'top') {
1395 + $this->Body = $attachStringHTML.'<br />'.$this->Body;
1396 + } else {
1397 + $this->Body .= '<br />'.$attachStringHTML;
1398 + }
1387 1399 }
1388 1400 }
1389 1401
1390 - private function generateAllParams()
1402 + private function replaceDynamicContent(int $mailId): void
1391 1403 {
1392 - $result = '<table style="border:1px solid;border-collapse:collapse;" border="1" cellpadding="10"><tr><td>Tag</td><td>Value</td></tr>';
1393 - foreach ($this->parameters as $name => $value) {
1394 - if (!is_string($value)) continue;
1404 + $this->replaceParams();
1395 1405
1396 - $result .= '<tr><td>'.trim($name, '{}').'</td><td>'.$value.'</td></tr>';
1406 + // Left side are mail columns on which we replace the short codes
1407 + $this->body = &$this->Body;
1408 + $this->altbody = &$this->AltBody;
1409 + $this->subject = &$this->Subject;
1410 + $this->from = &$this->From;
1411 + $this->fromName = &$this->FromName;
1412 + $this->replyto = &$this->ReplyTo;
1413 + $this->replyname = $this->defaultMail[$mailId]->reply_to_name ?? '';
1414 + $this->replyemail = $this->defaultMail[$mailId]->reply_to_email ?? '';
1415 + $this->mailId = intval($this->defaultMail[$mailId]->id);
1416 + $this->id = $this->mailId;
1417 + $this->creator_id = intval($this->defaultMail[$mailId]->creator_id);
1418 + $this->type = $this->defaultMail[$mailId]->type;
1419 + $this->stylesheet = &$this->stylesheet;
1420 + $this->links_language = $this->defaultMail[$mailId]->links_language;
1421 + }
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 +
1432 + private function prepareTracking(int $mailId, object $receiver): void
1433 + {
1434 + if ($this->canTrack($mailId, $receiver)) {
1435 + $receiver->id = intval($receiver->id);
1436 + $this->statPicture($this->mailId, $receiver->id);
1437 + $this->body = acym_absoluteURL($this->body);
1438 + $this->statClick($this->mailId, $receiver->id, false, $receiver->key ?? '');
1439 + if (acym_isTrackingSalesActive()) {
1440 + $this->trackingSales($this->mailId, $receiver->id);
1441 + }
1397 1442 }
1398 - $result .= '</table>';
1399 - $this->addParam('allshortcodes', $result);
1400 1443 }
1401 1444
1402 - public function addParamInfo()
1445 + private function handleDtexts(object $receiver): bool
1403 1446 {
1404 - if (!empty($_SERVER)) {
1405 - $serverinfo = [];
1406 - foreach ($_SERVER as $oneKey => $oneInfo) {
1407 - $serverinfo[] = $oneKey.' => '.strip_tags(print_r($oneInfo, true));
1447 + $this->replaceParams();
1448 +
1449 + // Sending a spam-test, use the current user instead
1450 + if (strpos($receiver->email, '@mt.acyba.com') !== false) {
1451 + $currentUser = $this->userClass->getOneByEmail(acym_currentUserEmail());
1452 + if (empty($currentUser)) {
1453 + $currentUser = $receiver;
1408 1454 }
1409 - $this->addParam('serverinfo', implode('<br />', $serverinfo));
1455 + $result = acym_trigger('replaceUserInformation', [&$this, &$currentUser, true]);
1456 + } else {
1457 + $result = acym_trigger('replaceUserInformation', [&$this, &$receiver, true]);
1458 + if (!empty($result)) {
1459 + foreach ($result as $oneResult) {
1460 + if (!empty($oneResult) && !$oneResult['send']) {
1461 + $this->reportMessage = $oneResult['message'];
1462 +
1463 + return false;
1464 + }
1465 + }
1466 + }
1410 1467 }
1411 1468
1412 - if (!empty($_REQUEST)) {
1413 - $postinfo = [];
1414 - foreach ($_REQUEST as $oneKey => $oneInfo) {
1415 - $postinfo[] = $oneKey.' => '.strip_tags(print_r($oneInfo, true));
1469 + global $acymLanguages;
1470 + if (!empty($acymLanguages['userLanguage'])) {
1471 + unset($acymLanguages['userLanguage']);
1472 + }
1473 +
1474 + // A user based dtext may have inserted HTML content, we need to re-inline the CSS to it
1475 + foreach ($result as $oneResult) {
1476 + if (!empty($oneResult['emogrifier'])) {
1477 + $this->prepareEmailContent();
1478 + break;
1416 1479 }
1417 - $this->addParam('postinfo', implode('<br />', $postinfo));
1418 1480 }
1481 +
1482 + if ($this->config->get('multiple_part', false)) {
1483 + $bodyForText = preg_replace('#<head\b[^>]*>.*?</head\s*>#is', '', $this->Body);
1484 + $this->altbody = $this->textVersion($bodyForText);
1485 + }
1486 +
1487 + $this->replaceParams();
1488 +
1489 + return true;
1419 1490 }
1420 1491
1421 - public function addParam($name, $value)
1492 + private function initStatistics(bool $status, int $receiverId): void
1422 1493 {
1423 - $tagName = '{'.$name.'}';
1424 - $this->parameters[$tagName] = $value;
1494 + if ($this->trackEmail) {
1495 + $statsAdd = [];
1496 + $statsAdd[$this->mailId][intval($status)][] = $receiverId;
1497 +
1498 + $queueHelper = new QueueHelper();
1499 + $queueHelper->statsAdd($statsAdd);
1500 + $this->trackEmail = false;
1501 + }
1425 1502 }
1426 1503
1427 - public function overrideEmail(array $options): bool
1504 + private function trackingSales(int $mailId, int $userId): void
1428 1505 {
1429 - $overrideAllEmails = $this->config->get('send_website_emails', 0) == 1;
1506 + preg_match_all('#href[ ]*=[ ]*"(?!mailto:|\#|ymsgr:|callto:|file:|ftp:|webcal:|skype:|tel:)([^"]+)"#Ui', $this->body, $results);
1507 + if (empty($results)) {
1508 + return;
1509 + }
1430 1510
1431 - $overrideClass = new OverrideClass();
1432 - $override = $overrideClass->getMailByBaseContent($options['subject'], $options['message']);
1511 + foreach ($results[1] as $url) {
1512 + $simplifiedUrl = str_replace(['https://', 'http://', 'www.'], '', $url);
1513 + $simplifiedWebsite = str_replace(['https://', 'http://', 'www.'], '', ACYM_LIVE);
1514 + if (strpos($simplifiedUrl, rtrim($simplifiedWebsite, '/')) === false || strpos($url, 'task=unsub')) continue;
1433 1515
1434 - if (empty($override) && !$overrideAllEmails) {
1435 - return false;
1516 + $toAddUrl = (strpos($url, '?') === false ? '?' : '&').'linkReferal='.$mailId.'-'.$userId;
1517 +
1518 + $posHash = strpos($url, '#');
1519 + if ($posHash !== false) {
1520 + $newURL = substr($url, 0, $posHash).$toAddUrl.substr($url, $posHash);
1521 + } else {
1522 + $newURL = $url.$toAddUrl;
1523 + }
1524 +
1525 + $this->body = preg_replace('#href="('.preg_quote($url, '#').')"#Uis', 'href="'.$newURL.'"', $this->body);
1436 1526 }
1527 + }
1437 1528
1438 - $this->autoAddUser = true;
1529 + private function statPicture(int $mailId, int $userId): void
1530 + {
1531 + $pictureLink = acym_frontendLink('frontstats&task=openStats&id='.$mailId.'&userid='.$userId, true, false);
1439 1532
1440 - if (empty($override)) {
1441 - $this->trackEmail = false;
1533 + $statPicture = '<img class="spict" alt="" src="'.$pictureLink.'" border="0" width="50" height="1" />';
1442 1534
1443 - return $this->overrideAllEmails($options);
1535 + if (strpos($this->body, '</body>')) {
1536 + $this->body = str_replace('</body>', $statPicture.'</body>', $this->body);
1444 1537 } else {
1445 - $this->trackEmail = true;
1538 + $this->body .= $statPicture;
1539 + }
1540 + }
1446 1541
1447 - return $this->sendOverride($override, $options);
1542 + private function removeAdditionalParams(string $url): string
1543 + {
1544 + $additionalParamsPos = strpos($url, '?');
1545 + if (!empty($additionalParamsPos)) {
1546 + $url = substr($url, 0, $additionalParamsPos);
1448 1547 }
1548 +
1549 + return $url;
1449 1550 }
1450 1551
1552 + private function replaceParams(): void
1553 + {
1554 + if (empty($this->parameters)) {
1555 + return;
1556 + }
1557 +
1558 + $pluginHelper = new PluginHelper();
1559 +
1560 + //We create an extra tag which contains all possible parameters...
1561 + $this->generateAllParams();
1562 +
1563 + $vars = [
1564 + 'Subject',
1565 + 'Body',
1566 + 'From',
1567 + 'FromName',
1568 + 'replyname',
1569 + 'replyemail',
1570 + ];
1571 +
1572 + foreach ($vars as $oneVar) {
1573 + if (!empty($this->$oneVar)) {
1574 + $this->$oneVar = $pluginHelper->replaceDText($this->$oneVar, $this->parameters);
1575 + }
1576 + }
1577 +
1578 + if (!empty($this->ReplyTo)) {
1579 + foreach ($this->ReplyTo as $i => $replyto) {
1580 + foreach ($replyto as $a => $oneval) {
1581 + $this->ReplyTo[$i][$a] = $pluginHelper->replaceDText($this->ReplyTo[$i][$a], $this->parameters);
1582 + }
1583 + }
1584 + }
1585 + }
1586 +
1587 + /**
1588 + * Create a new parameter called {alltags} which will include all the others
1589 + */
1590 + private function generateAllParams(): void
1591 + {
1592 + $result = '<table style="border:1px solid;border-collapse:collapse;" border="1" cellpadding="10"><tr><td>Tag</td><td>Value</td></tr>';
1593 + foreach ($this->parameters as $name => $value) {
1594 + //Just in case of...
1595 + if (!is_string($value)) continue;
1596 +
1597 + $result .= '<tr><td>'.trim($name, '{}').'</td><td>'.$value.'</td></tr>';
1598 + }
1599 + $result .= '</table>';
1600 + $this->addParam('allshortcodes', $result);
1601 + }
1602 +
1451 1603 private function sendOverride(object $override, array $options): bool
1452 1604 {
1453 - for ($i = 1 ; $i < count($override->parameters) ; $i++) {
1605 + // 2 - Prepare the email and params
1606 + for ($i = 1; $i < count($override->parameters); $i++) {
1454 1607 $oneParam = $override->parameters[$i];
1455 1608
1609 + // Joomla emails have links as text, convert them
1456 1610 $unmodified = $oneParam;
1457 1611 $oneParam = preg_replace(
1458 1612 '/(http|https):\/\/(.*)/',
1459 1613 '<a href="$1://$2" target="_blank">$1://$2</a>',
@@ -1468,11 +1622,13 @@
1468 1622 }
1469 1623
1470 1624 $this->addParam('subject', $options['subject']);
1471 1625
1626 + // 3 - Send the email
1472 1627 $this->overrideEmailToSend = $override;
1473 1628 $statusSend = $this->sendOne($override->id, $options['to'], $options);
1474 1629 if (!$statusSend && !empty($this->reportMessage)) {
1630 + // Something went wrong when trying to send the override, log the information in the cron logs file
1475 1631 $cronHelper = new CronHelper();
1476 1632 $cronHelper->saveReport([$this->reportMessage]);
1477 1633 }
1478 1634
@@ -1478,26 +1634,27 @@
1478 1634
1479 1635 return $statusSend;
1480 1636 }
1481 1637
1482 - private function getSendSettings($type, $mailId = 0)
1638 + public function getSendSettings(string $type, int $mailId = 0): string
1483 1639 {
1484 - if (!in_array($type, ['from_name', 'from_email', 'replyto_name', 'replyto_email'])) return false;
1640 + if (!in_array($type, ['from_name', 'from_email', 'replyto_name', 'replyto_email', 'bounce_email'])) {
1641 + return '';
1642 + }
1485 1643
1486 1644 $mailType = strpos($type, 'replyto') !== false ? str_replace('replyto', 'reply_to', $type) : $type;
1645 + if (!empty($mailId) && !empty($this->defaultMail[$mailId]->$mailType)) {
1646 + return $this->defaultMail[$mailId]->$mailType;
1647 + }
1487 1648
1488 - if (!empty($mailId) && !empty($this->defaultMail[$mailId]) && !empty($this->defaultMail[$mailId]->$mailType)) return $this->defaultMail[$mailId]->$mailType;
1489 -
1490 - $lang = empty($this->userLanguage) ? acym_getLanguageTag() : $this->userLanguage;
1491 -
1649 + $lang = empty($this->userLanguage) ? acym_getDefaultNewsletterLanguage() : $this->userLanguage;
1492 1650 $setting = $this->config->get($type);
1493 -
1494 1651 $translation = $this->config->get('sender_info_translation');
1495 1652
1496 1653 if (!empty($translation)) {
1497 1654 $translation = json_decode($translation, true);
1498 1655
1499 - if (!empty($translation[$lang])) {
1656 + if (!empty($translation[$lang][$type])) {
1500 1657 $setting = $translation[$lang][$type];
1501 1658 }
1502 1659 }
1503 1660
@@ -1503,9 +1660,9 @@
1503 1660
1504 1661 return $setting;
1505 1662 }
1506 1663
1507 - private function prepareEmailContent(?int $mailId = null): void
1664 + private function prepareEmailContent(int $mailId = 0): void
1508 1665 {
1509 1666 $this->handleRelativeURLs($mailId);
1510 1667 $this->inlineCSS($mailId);
1511 1668 $this->finalizeHtmlStructure($mailId);
@@ -1510,25 +1667,31 @@
1510 1667 $this->inlineCSS($mailId);
1511 1668 $this->finalizeHtmlStructure($mailId);
1512 1669 }
1513 1670
1514 - private function handleRelativeURLs(?int $mailId = null): void
1671 + private function handleRelativeURLs(int $mailId): void
1515 1672 {
1516 1673 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1517 1674 $mail->body = acym_absoluteURL($mail->body);
1518 1675 }
1519 1676
1520 - private function inlineCSS(?int $mailId = null): void
1677 + private function inlineCSS(int $mailId): void
1521 1678 {
1522 1679 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1680 + if (empty($mail->body)) {
1681 + return;
1682 + }
1523 1683
1524 - global $emogrifiedMediaCSS;
1525 - $emogrifiedMediaCSS = '';
1684 + global $acymailingEmogrifiedMediaCSS;
1685 + $acymailingEmogrifiedMediaCSS = '';
1526 1686
1527 1687 $style = $this->getEmailStylesheet($mail);
1528 - $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));
1529 1691 $domDocument = $cssInliner->getDomDocument();
1530 1692
1693 + // Remove the TinyMCE ids and the trailing ; in styles
1531 1694 $mail->body = preg_replace(
1532 1695 [
1533 1696 '# id="mce_\d+"#Ui',
1534 1697 '#(style="[^"]+);"#Ui',
@@ -1541,10 +1704,12 @@
1541 1704 ->removeRedundantClassesAfterCssInlined($cssInliner)
1542 1705 ->renderBodyContent()
1543 1706 );
1544 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
1545 1709 $mail->body = str_replace('&zwj;', '', $mail->body);
1546 1710
1711 + // Remove the extra spaces in the style attributes
1547 1712 $mail->body = preg_replace_callback(
1548 1713 '#style="([^"]+)"#Ui',
1549 1714 function ($matches) {
1550 1715 return 'style="'.str_replace(['; ', ': '], [';', ':'], $matches[1]).'"';
@@ -1552,9 +1717,9 @@
1552 1717 $mail->body
1553 1718 );
1554 1719 }
1555 1720
1556 - private function finalizeHtmlStructure(?int $mailId = null): void
1721 + private function finalizeHtmlStructure(int $mailId): void
1557 1722 {
1558 1723 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1559 1724
1560 1725 $finalContent = '<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">';
@@ -1563,10 +1728,13 @@
1563 1728 $finalContent .= '<meta http-equiv="Content-Type" content="text/html; charset='.strtolower($this->config->get('charset')).'" />'."\n";
1564 1729 $finalContent .= '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'."\n";
1565 1730 $finalContent .= '<title>'.$mail->subject.'</title>'."\n";
1566 1731
1567 - global $emogrifiedMediaCSS;
1568 - $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>';
1569 1737 $finalContent .= '<!--[if mso]><style type="text/css">#acym__wysid__template center > table { width: 580px; }</style><![endif]-->';
1570 1738 $finalContent .= '<!--[if !mso]><!--><style>#acym__wysid__template center > table { width: 100%; }</style><!--<![endif]-->';
1571 1739
1572 1740 if (!empty($mail->headers)) {
@@ -1593,12 +1761,14 @@
1593 1761 private function getEmailStylesheet(object $mail): array
1594 1762 {
1595 1763 $style = [];
1596 1764
1765 + // If this is a drag and drop mail we add foundation css for email
1597 1766 if (strpos($mail->body, 'acym__wysid__template') !== false) {
1598 1767 static $foundationCSS = null;
1599 1768 if (empty($foundationCSS)) {
1600 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
1601 1771 $foundationCSS = str_replace('#acym__wysid__template ', '', $foundationCSS);
1602 1772 }
1603 1773 $style['foundation'] = $foundationCSS;
1604 1774 }
@@ -1620,9 +1790,67 @@
1620 1790
1621 1791 return $style;
1622 1792 }
1623 1793
1794 + private function overrideAllEmails(array $options): bool
1795 + {
1796 + $this->trackEmail = false;
1624 1797
1798 + $this->addParam('subject', $options['subject']);
1799 +
1800 + if (isset($options['isHtml']) && !$options['isHtml']) {
1801 + $options['message'] = nl2br($options['message']);
1802 + }
1803 + $this->addParam('body', $options['message']);
1804 +
1805 + $mailClass = new MailClass();
1806 + $this->overrideEmailToSend = $mailClass->getOneByName('acy_notification_cms');
1807 +
1808 + if (empty($this->overrideEmailToSend)) {
1809 + return false;
1810 + }
1811 +
1812 + $statusSend = $this->sendOne($this->overrideEmailToSend->id, $options['to'], $options);
1813 + if (!$statusSend && !empty($this->reportMessage)) {
1814 + // Something went wrong when trying to send the override, log the information in the cron logs file
1815 + $cronHelper = new CronHelper();
1816 + $cronHelper->addMessage($this->reportMessage);
1817 + $cronHelper->saveReport();
1818 + }
1819 +
1820 + return $statusSend;
1821 + }
1822 +
1823 + /* * * * * * * * * * * * * * * * *
1824 + * *
1825 + * Override PHPMailer's methods *
1826 + * *
1827 + * * * * * * * * * * * * * * * * */
1828 +
1829 + /**
1830 + * @throws Exception
1831 + */
1832 + private function acymAddReplyTo(string $email, string $name): void
1833 + {
1834 + if (empty($email)) {
1835 + return;
1836 + }
1837 +
1838 + $replyToName = $this->config->get('add_names', true) ? $this->cleanText(trim($name)) : '';
1839 + $replyToEmail = trim($email);
1840 +
1841 + if (substr_count($replyToEmail, '@') > 1) {
1842 + //We have more than one reply to...
1843 + $replyToEmailArray = explode(';', str_replace([';', ','], ';', $replyToEmail));
1844 + $replyToNameArray = explode(';', str_replace([';', ','], ';', $replyToName));
1845 + foreach ($replyToEmailArray as $i => $oneReplyTo) {
1846 + $this->addReplyTo($this->cleanText($oneReplyTo), @$replyToNameArray[$i]);
1847 + }
1848 + } else {
1849 + $this->addReplyTo($this->cleanText($replyToEmail), $replyToName);
1850 + }
1851 + }
1852 +
1625 1853 public function setFrom($email, $name = '', $auto = false)
1626 1854 {
1627 1855 if (!empty($email)) {
1628 1856 $this->From = $this->cleanText($email);
@@ -1631,8 +1859,13 @@
1631 1859 $this->FromName = $this->cleanText($name);
1632 1860 }
1633 1861 }
1634 1862
1863 + /**
1864 + * Outputs debugging info via user-defined method
1865 + *
1866 + * @param string $str
1867 + */
1635 1868 protected function edebug($str)
1636 1869 {
1637 1870 if (strpos($this->ErrorInfo, $str) === false) {
1638 1871 $this->ErrorInfo .= ' '.$str;
@@ -1642,8 +1875,9 @@
1642 1875 public function getMailMIME()
1643 1876 {
1644 1877 $result = parent::getMailMIME();
1645 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
1646 1880 $result = rtrim($result, static::$LE);
1647 1881
1648 1882 if ($this->Mailer != 'mail') {
1649 1883 $result .= static::$LE.static::$LE;
@@ -1654,34 +1888,6 @@
1654 1888
1655 1889 public static function validateAddress($address, $patternselect = null)
1656 1890 {
1657 1891 return true;
1658 - }
1659 -
1660 - private function overrideAllEmails(array $options): bool
1661 - {
1662 - $this->trackEmail = false;
1663 -
1664 - $this->addParam('subject', $options['subject']);
1665 -
1666 - if (isset($options['isHtml']) && !$options['isHtml']) {
1667 - $options['message'] = nl2br($options['message']);
1668 - }
1669 - $this->addParam('body', $options['message']);
1670 -
1671 - $mailClass = new MailClass();
1672 - $this->overrideEmailToSend = $mailClass->getOneByName('acy_notification_cms');
1673 -
1674 - if (empty($this->overrideEmailToSend)) {
1675 - return false;
1676 - }
1677 -
1678 - $statusSend = $this->sendOne($this->overrideEmailToSend->id, $options['to'], $options);
1679 - if (!$statusSend && !empty($this->reportMessage)) {
1680 - $cronHelper = new CronHelper();
1681 - $cronHelper->addMessage($this->reportMessage);
1682 - $cronHelper->saveReport();
1683 - }
1684 -
1685 - return $statusSend;
1686 1892 }
1687 1893 }