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 +1893 -0 10.10.1 → 11.1.0 View file →
@@ -1,0 +1,1893 @@
1 +<?php
2 +
3 +namespace AcyMailing\Helpers;
4 +
5 +use AcyMailing\Classes\CampaignClass;
6 +use AcyMailing\Classes\ConfigurationClass;
7 +use AcyMailing\Classes\FollowupClass;
8 +use AcyMailing\Classes\MailArchiveClass;
9 +use AcyMailing\Classes\MailClass;
10 +use AcyMailing\Classes\OverrideClass;
11 +use AcyMailing\Classes\UrlClass;
12 +use AcyMailing\Classes\UserClass;
13 +use AcyMailerPhp\Exception;
14 +use AcyMailerPhp\OAuth;
15 +use AcyMailerPhp\Mailer;
16 +use AcyMailing\Libraries\Pelago\Emogrifier\CssInliner;
17 +use AcyMailing\Libraries\Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
18 +
19 +class MailerHelper extends Mailer
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';
24 +
25 + // Redefine PHPMailer protected attributes for dynamic texts system
26 + public $to = [];
27 + public $cc = [];
28 + public $bcc = [];
29 + public $ReplyTo = [];
30 + public $attachment = [];
31 + public $CustomHeader = [];
32 +
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;
43 +
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 +
61 + public string $reportMessage = '';
62 + public bool $dtextsFailed = false;
63 +
64 + // Should we track the sending of a message (used for welcoming message)
65 + public bool $trackEmail = false;
66 +
67 + // Sending method used in the configuration
68 + public string $externalMailer;
69 +
70 + // To import custom stylesheet from user
71 + public string $stylesheet = '';
72 + public array $settings;
73 +
74 + // Used to store special dynamic text content
75 + public array $parameters = [];
76 +
77 + private string $userLanguage = '';
78 + private string $receiverEmail;
79 +
80 + // Special send statuses
81 + private object $overrideEmailToSend;
82 + public bool $isTest = false;
83 + public bool $isSpamTest = false;
84 + public bool $isForward = false;
85 +
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 = [];
94 +
95 + // Sending method list plugin
96 + public array $listsIds = [];
97 + private string $currentSendingMethod = '';
98 + private array $currentMethodSetting = [];
99 +
100 + public bool $isAbTest = false;
101 + public bool $isTransactional = false;
102 + public bool $isOneTimeMail = false;
103 + public bool $isSendingMethodByListActive = false;
104 + public string $portError;
105 +
106 + public function __construct()
107 + {
108 + parent::__construct();
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 +
114 + $this->encodingHelper = new EncodingHelper();
115 + $this->editorHelper = new EditorHelper();
116 + $this->userClass = new UserClass();
117 + $this->mailClass = new MailClass();
118 + $this->mailArchiveClass = new MailArchiveClass();
119 + $this->config = acym_config();
120 +
121 + $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
122 + $this->Sender = $this->cleanText($this->getSendSettings('bounce_email'));
123 + if (empty($this->Sender)) {
124 + $this->Sender = '';
125 + }
126 + $maxLineLength = $this->config->get('mailer_wordwrap', 0);
127 + if (!empty($maxLineLength) && self::MAX_LINE_LENGTH > $maxLineLength) {
128 + $this->WordWrap = $maxLineLength;
129 + }
130 +
131 + $this->setSendingMethodSetting();
132 +
133 + //Do we have a DKIM validation?
134 + if ($this->config->get('dkim', 0) && $this->Mailer != 'elasticemail') {
135 + $this->DKIM_domain = $this->config->get('dkim_domain');
136 + $this->DKIM_selector = $this->config->get('dkim_selector', 'acy');
137 + //Just in case of...
138 + if (empty($this->DKIM_selector)) $this->DKIM_selector = 'acy';
139 + $this->DKIM_passphrase = $this->config->get('dkim_passphrase');
140 + $this->DKIM_identity = $this->config->get('dkim_identity');
141 + $this->DKIM_private = trim($this->config->get('dkim_private'));
142 + $this->DKIM_private_string = trim($this->config->get('dkim_private'));
143 + }
144 +
145 + //Set the Charset, by default 'utf-8'
146 + $this->CharSet = strtolower($this->config->get('charset'));
147 + if (empty($this->CharSet)) {
148 + $this->CharSet = 'utf-8';
149 + }
150 +
151 + $this->clearAll();
152 +
153 + //Set the encoding format, should be 8 bit by default.
154 + $this->Encoding = $this->config->get('encoding_format');
155 + if (empty($this->Encoding)) {
156 + $this->Encoding = '8bit';
157 + }
158 +
159 + // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- This is for big regex, the default value is 100 000.
160 + @ini_set('pcre.backtrack_limit', 1000000);
161 +
162 + $this->SMTPOptions = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true]];
163 + }
164 +
165 + public function setSendingMethodSetting(): void
166 + {
167 + $externalSendingMethod = [];
168 + acym_trigger('onAcymGetSendingMethods', [&$externalSendingMethod, true]);
169 + $externalSendingMethod = !empty($externalSendingMethod['sendingMethods']) ? array_keys($externalSendingMethod['sendingMethods']) : [];
170 +
171 + $mailerMethodConfig = $this->getSendingMethod();
172 + $this->currentSendingMethod = $mailerMethodConfig;
173 +
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';
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();
233 + $this->Host = trim($this->getSendingMethodSettings('smtp_host'));
234 + $port = $this->getSendingMethodSettings('smtp_port');
235 + // 465 is default port for SSL
236 + if (empty($port) && $this->getSendingMethodSettings('smtp_secured') === 'ssl') {
237 + $port = 465;
238 + }
239 + if (!empty($port)) {
240 + $this->Host .= ':'.$port;
241 + }
242 + $this->SMTPAuth = (bool)$this->getSendingMethodSettings('smtp_auth', '1');
243 + $this->Username = trim($this->getSendingMethodSettings('smtp_username'));
244 +
245 + $authMethod = $this->getSendingMethodSettings('smtp_method');
246 + if (in_array($authMethod, ['CRAM-MD5', 'LOGIN', 'PLAIN'])) {
247 + $this->AuthType = $authMethod;
248 + }
249 + if ($this->SMTPAuth) {
250 + $this->Password = trim($this->getSendingMethodSettings('smtp_password'));
251 + }
252 +
253 + $this->SMTPSecure = trim($this->getSendingMethodSettings('smtp_secured'));
254 +
255 + if (empty($this->Sender)) {
256 + $this->Sender = strpos($this->Username, '@') ? $this->Username : $this->getSendingMethodSettings('from_email');
257 + }
258 + } elseif ($mailerMethodConfig === 'sendmail') {
259 + $this->isSendmail();
260 + $this->Sendmail = trim($this->getSendingMethodSettings('sendmail_path'));
261 + if (empty($this->Sendmail)) {
262 + $this->Sendmail = '/usr/sbin/sendmail';
263 + }
264 + } elseif ($mailerMethodConfig === 'qmail') {
265 + $this->isQmail();
266 + } elseif ($mailerMethodConfig === 'amazon') {
267 + $this->isSMTP();
268 + $amazonCredentials = [];
269 + acym_trigger('onAcymGetCredentialsSendingMethod', [&$amazonCredentials, 'amazon', $this->currentMethodSetting], 'plgAcymAmazon');
270 + $this->Host = trim($amazonCredentials['amazon_host']).':587';
271 + $this->Username = trim($amazonCredentials['amazon_username']);
272 + $this->Password = trim($amazonCredentials['amazon_password']);
273 + $this->SMTPAuth = true;
274 + $this->SMTPSecure = 'tls';
275 + } elseif ($mailerMethodConfig === 'brevo-smtp') {
276 + $this->isSMTP();
277 + $brevoSmtpCredentials = [];
278 + acym_trigger('onAcymGetCredentialsSendingMethod', [&$brevoSmtpCredentials, 'brevo-smtp', $this->currentMethodSetting], 'plgAcymBrevo');
279 + $this->Host = trim($brevoSmtpCredentials['brevo-smtp_host']).':587';
280 + $this->Username = trim($brevoSmtpCredentials['brevo-smtp_username']);
281 + $this->Password = trim($brevoSmtpCredentials['brevo-smtp_password']);
282 + $this->SMTPAuth = true;
283 + $this->SMTPSecure = 'tls';
284 + } elseif (in_array($mailerMethodConfig, $externalSendingMethod)) {
285 + $this->isExternal($mailerMethodConfig);
286 + } else {
287 + $this->isMail();
288 + }
289 + }
290 +
291 + /**
292 + * Dynamically called from PHPMailer
293 + */
294 + protected function externalSend(string $MIMEHeader, string $MIMEBody): bool
295 + {
296 + $fromName = empty($this->FromName) ? $this->config->get('from_name') : $this->FromName;
297 + $reply_to = array_shift($this->ReplyTo);
298 +
299 + $attachments = [];
300 + if (!empty($this->attachment) && $this->config->get('embed_files')) {
301 + foreach ($this->attachment as $i => $oneAttach) {
302 + $encodedContent = $this->encodeFile($oneAttach[0], $oneAttach[3]);
303 + $this->attachment[$i]['contentEncoded'] = $encodedContent;
304 + }
305 + $attachments = $this->attachment;
306 + }
307 +
308 + $response = [];
309 + $data = [
310 + &$response,
311 + $this,
312 + ['email' => $this->to[0][0], 'name' => $this->to[0][1]],
313 + ['email' => $this->From, 'name' => $fromName],
314 + ['email' => $reply_to[0], 'name' => $reply_to[1]],
315 + !empty($this->bcc) ? $this->bcc : [],
316 + $attachments,
317 + $this->currentMethodSetting,
318 + ];
319 + acym_trigger('onAcymSendEmail', $data);
320 +
321 + if (!empty($response['error'])) {
322 + $this->setError($response['message']);
323 +
324 + return false;
325 + }
326 +
327 + return true;
328 + }
329 +
330 + public function send(): bool
331 + {
332 + if (!file_exists(ACYM_LIBRARIES.'Mailer'.DS.'Mailer.php')) {
333 + $this->reportMessage = acym_translationSprintf('ACYM_X_FILE_MISSING', 'Mailer', ACYM_LIBRARIES.'Mailer'.DS);
334 + if ($this->report) {
335 + acym_enqueueMessage($this->reportMessage, 'error');
336 + }
337 +
338 + return false;
339 + }
340 +
341 + if (empty($this->Subject) || empty($this->Body)) {
342 + if ($this->isTest && empty($this->Subject)) {
343 + $this->Subject = acym_translation('ACYM_EMAIL_SUBJECT');
344 + } else {
345 + $this->reportMessage = acym_translation('ACYM_SEND_EMPTY');
346 + $this->errorNumber = 8;
347 + if ($this->report) {
348 + acym_enqueueMessage($this->reportMessage, 'error');
349 + }
350 +
351 + return false;
352 + }
353 + }
354 +
355 + //Check if there is at least one reply to otherwise add the default one.
356 + if (empty($this->ReplyTo) && empty($this->ReplyToQueue)) {
357 + if (!empty($this->replyemail)) {
358 + $replyToEmail = $this->replyemail;
359 + } elseif ($this->config->get('from_as_replyto', 1) == 1) {
360 + $replyToEmail = $this->getSendSettings('from_email');
361 + } else {
362 + $replyToEmail = $this->getSendSettings('replyto_email');
363 + }
364 +
365 + if (!empty($this->replyname)) {
366 + $replyToName = $this->replyname;
367 + } elseif ($this->config->get('from_as_replyto', 1) == 1) {
368 + $replyToName = $this->getSendSettings('from_name');
369 + } else {
370 + $replyToName = $this->getSendSettings('replyto_name');
371 + }
372 +
373 + $this->acymAddReplyTo($replyToEmail, $replyToName);
374 + }
375 +
376 + // Embed images if there are images to embed
377 + $shouldEmbed = $this->config->get('embed_images', 0);
378 + if (intval($shouldEmbed) === 1 && $this->Mailer !== 'elasticemail') {
379 + $this->embedImages();
380 + }
381 +
382 + if (!$this->alreadyCheckedAddresses) {
383 + $this->alreadyCheckedAddresses = true;
384 +
385 + $replyToTmp = '';
386 + if (!empty($this->ReplyTo)) {
387 + $replyToTmp = reset($this->ReplyTo);
388 + $replyToTmp = $replyToTmp[0];
389 + } elseif (!empty($this->ReplyToQueue)) {
390 + $replyToTmp = reset($this->ReplyToQueue);
391 + $replyToTmp = $replyToTmp[1];
392 + }
393 +
394 + if (empty($replyToTmp) || !acym_isValidEmail($replyToTmp)) {
395 + $this->reportMessage = acym_translation('ACYM_VALID_EMAIL').' ( '.acym_translation('ACYM_REPLYTO_EMAIL').' : '.(empty($this->ReplyTo) ? '' : $replyToTmp).' ) ';
396 + $this->errorNumber = 9;
397 + if ($this->report) {
398 + acym_enqueueMessage($this->reportMessage, 'error');
399 + }
400 +
401 + return false;
402 + }
403 +
404 + //Check the from address
405 + if (empty($this->From) || !acym_isValidEmail($this->From)) {
406 + $this->reportMessage = acym_translation('ACYM_VALID_EMAIL').' ( '.acym_translation('ACYM_FROM_EMAIL').' : '.$this->From.' ) ';
407 + $this->errorNumber = 9;
408 + if ($this->report) {
409 + acym_enqueueMessage($this->reportMessage, 'error');
410 + }
411 +
412 + return false;
413 + }
414 +
415 + if (!empty($this->Sender) && !acym_isValidEmail($this->Sender)) {
416 + $this->reportMessage = acym_translation('ACYM_VALID_EMAIL').' ( '.acym_translation('ACYM_BOUNCE_EMAIL').' : '.$this->Sender.' ) ';
417 + $this->errorNumber = 9;
418 + if ($this->report) {
419 + acym_enqueueMessage($this->reportMessage, 'error');
420 + }
421 +
422 + return false;
423 + }
424 + }
425 +
426 + if ($this->config->get('save_body') === '1') {
427 + @file_put_contents(ACYM_ROOT.'acydebug_mail.html', $this->Body);
428 + }
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!
432 + $this->Body = htmlentities($this->Body);
433 + $this->Body = htmlspecialchars_decode($this->Body);
434 + //Fix The Bat issues for special encoding as &sigmaf; was interpreted as &sigma;f;...
435 + $this->Body = str_replace(['&amp;', '&sigmaf;'], ['&', 'ς'], $this->Body);
436 +
437 + if ($this->CharSet !== 'utf-8') {
438 + $this->Body = $this->encodingHelper->change($this->Body, 'UTF-8', $this->CharSet);
439 + $this->Subject = $this->encodingHelper->change($this->Subject, 'UTF-8', $this->CharSet);
440 + }
441 +
442 + // These characters can break the send process, let's remove them from the subject
443 + $this->Subject = str_replace(
444 + ['’', '“', '”', '–'],
445 + ["'", '"', '"', '-'],
446 + $this->Subject
447 + );
448 +
449 + // BE CAREFUL! This space is not a space, it's a ALT0160 chr(194) by char(32) which means almost &nbsp;
450 + $this->Body = str_replace(" ", ' ', $this->Body);
451 +
452 + $externalSending = false;
453 +
454 + $this->isOneTimeMail = $this->isTransactional = $this->isForward || $this->isTest || $this->isSpamTest;
455 + if (!empty($this->mailId) && !empty($this->defaultMail[$this->mailId])) {
456 + if ($this->mailClass->isTransactionalMail($this->defaultMail[$this->mailId])) {
457 + $this->isTransactional = true;
458 + }
459 +
460 + if ($this->mailClass->isOneTimeMail($this->defaultMail[$this->mailId])) {
461 + $this->isOneTimeMail = true;
462 + }
463 + }
464 +
465 + acym_trigger('onAcymProcessQueueExternalSendingCampaign', [&$externalSending, $this->isTransactional]);
466 +
467 + $warnings = '';
468 +
469 + if (ACYM_PRODUCTION) {
470 + if ($externalSending) {
471 + $result = false;
472 + acym_trigger('onAcymRegisterReceiverContentAndList', [&$result, $this->Subject, $this->Body, $this->receiverEmail, $this->mailId, &$warnings]);
473 + } else {
474 + acym_trigger('onAcymBeforeEmailSend', [&$this]);
475 + ob_start();
476 + $result = parent::send();
477 + $warnings = ob_get_clean();
478 + }
479 + }
480 +
481 +
482 + //display error if bloque is displayed... for free.fr especially
483 + if (!empty($warnings) && strpos($warnings, 'bloque')) {
484 + $result = false;
485 + }
486 +
487 + $this->mailHeader = '';
488 +
489 + $receivers = [];
490 + foreach ($this->to as $oneReceiver) {
491 + $receivers[] = $oneReceiver[0];
492 + }
493 +
494 + if (!$result) {
495 + $this->reportMessage = acym_translationSprintf('ACYM_SEND_ERROR', '<b>'.$this->Subject.'</b>', '<b>'.implode(' , ', $receivers).'</b>');
496 + if (!empty($this->ErrorInfo)) {
497 + $this->reportMessage .= " \n\n ".$this->ErrorInfo;
498 + }
499 + if (!empty($warnings)) {
500 + $this->reportMessage .= " \n\n ".$warnings;
501 + }
502 + $this->errorNumber = 1;
503 + if ($this->report) {
504 + //We display the report here... we add a link to our doc for the "could not instantiate mail function".
505 + $this->reportMessage = str_replace(
506 + 'Could not instantiate mail function',
507 + '<a target="_blank" href="'.ACYM_DOCUMENTATION.'faq/could-not-instantiate-mail-function">'.acym_translation('ACYM_COUND_NOT_INSTANCIATE_MAIL_FUCNTION').'</a>',
508 + $this->reportMessage
509 + );
510 + acym_enqueueMessage(nl2br($this->reportMessage), 'error');
511 + }
512 + } else {
513 + if ($this->isSendingMethodByListActive) {
514 + $this->reportMessage = acym_translationSprintf(
515 + 'ACYM_SEND_SUCCESS_WITH_SENDING_METHOD',
516 + '<b>'.$this->Subject.'</b>',
517 + '<b>'.implode(' , ', $receivers).'</b>',
518 + '<b>'.$this->currentSendingMethod.'</b>'
519 + );
520 + } else {
521 + $this->reportMessage = acym_translationSprintf('ACYM_SEND_SUCCESS', '<b>'.$this->Subject.'</b>', '<b>'.implode(' , ', $receivers).'</b>');
522 + }
523 +
524 + if (!empty($warnings)) {
525 + $this->reportMessage .= " \n\n ".$warnings;
526 + }
527 +
528 + if ($this->report && acym_isAdmin()) {
529 + acym_enqueueMessage(preg_replace('#(<br( ?/)?>){2}#', '<br />', nl2br($this->reportMessage)), 'info');
530 + }
531 + }
532 +
533 + return $result;
534 + }
535 +
536 + public function clearAll(): void
537 + {
538 + $this->Subject = '';
539 + $this->Body = '';
540 + $this->AltBody = '';
541 + $this->ClearAllRecipients();
542 + $this->ClearAttachments();
543 + $this->ClearCustomHeaders();
544 + $this->ClearReplyTos();
545 + $this->errorNumber = 0;
546 + $this->MessageID = '';
547 + $this->ErrorInfo = '';
548 + $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
549 + }
550 +
551 + public function load(int $mailId, ?object $user = null): ?object
552 + {
553 + // If it's already loaded return the email
554 + if (isset($this->defaultMail[$mailId])) {
555 + return $this->defaultMail[$mailId];
556 + }
557 +
558 + if (!empty($this->overrideEmailToSend)) {
559 + $this->defaultMail[$mailId] = $this->overrideEmailToSend;
560 + } else {
561 + $this->defaultMail[$mailId] = $this->mailClass->getOneById($mailId, true);
562 + }
563 +
564 + global $acymLanguages;
565 + if (!$this->isAbTest && empty($this->overrideEmailToSend) && acym_isMultilingual()) {
566 + $defaultLanguage = $this->config->get('multilingual_default', ACYM_DEFAULT_LANGUAGE);
567 + $mails = $this->mailClass->getMultilingualMails($mailId);
568 +
569 + $this->userLanguage = !empty($user->language) ? $user->language : $defaultLanguage;
570 +
571 + if (!empty($mails)) {
572 + $languages = array_keys($mails);
573 + if (count($languages) == 1) {
574 + $key = $languages[0];
575 + } elseif (empty($mails[$this->userLanguage])) {
576 + $key = $defaultLanguage;
577 + } else {
578 + $key = $this->userLanguage;
579 + }
580 +
581 + if (isset($mails[$key])) {
582 + $this->defaultMail[$mailId] = $mails[$key];
583 + }
584 + } else {
585 + unset($this->defaultMail[$mailId]);
586 +
587 + return null;
588 + }
589 +
590 + $acymLanguages['userLanguage'] = $this->userLanguage;
591 + $this->setFrom($this->getSendSettings('from_email'), $this->getSendSettings('from_name'));
592 + }
593 +
594 + //We could not load the email...
595 + if (empty($this->defaultMail[$mailId]->id)) {
596 + unset($this->defaultMail[$mailId]);
597 +
598 + return null;
599 + }
600 +
601 + if (!empty($this->defaultMail[$mailId]->attachments)) {
602 + $this->defaultMail[$mailId]->attach = [];
603 +
604 + $attachments = json_decode($this->defaultMail[$mailId]->attachments, true);
605 + foreach ($attachments as $oneAttach) {
606 + $attach = new \stdClass();
607 + $attach->name = basename($oneAttach['filename']);
608 + $attach->filename = str_replace(['/', '\\'], DS, ACYM_ROOT).$oneAttach['filename'];
609 + $attach->url = ACYM_LIVE.$oneAttach['filename'];
610 + $this->defaultMail[$mailId]->attach[] = $attach;
611 + }
612 + }
613 +
614 +
615 + $this->mailId = $mailId;
616 + $this->id = $this->mailId;
617 + if (empty($this->defaultMail[$mailId]->language)) {
618 + $this->defaultMail[$mailId]->language = acym_getDefaultNewsletterLanguage();
619 + }
620 + $this->mail = clone $this->defaultMail[$mailId];
621 + acym_trigger('replaceContent', [&$this->defaultMail[$mailId], true]);
622 +
623 + $this->prepareEmailContent($mailId);
624 + if (!$this->isTest) {
625 + $this->storeArchiveVersion($mailId);
626 + }
627 +
628 + return $this->defaultMail[$mailId];
629 + }
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 + */
639 + public function sendOne($mailId, $user, array $options = []): bool
640 + {
641 + $this->clearAll();
642 +
643 + $receiver = $this->loadUser($user);
644 + $this->isTest = !empty($options['isTest']);
645 + $this->dtextsFailed = false;
646 +
647 + if (empty($receiver->email)) {
648 + $this->errorNumber = 4;
649 + $this->reportMessage = acym_translationSprintf('ACYM_SEND_ERROR_USER', '<b><i>'.esc_html(is_object($user) ? $user->email : $user).'</i></b>');
650 + if ($this->report) {
651 + acym_enqueueMessage($this->reportMessage, 'error');
652 + }
653 +
654 + return false;
655 + }
656 +
657 + // Can be a string for Joomla 3 and WordPress
658 + $mailId = intval($mailId);
659 + $receiver->id = intval($receiver->id);
660 +
661 + // Load the mail with global tags replaced
662 + if (!$this->load($mailId, $receiver)) {
663 + $this->reportMessage = 'Can not load the email with ID n°'.$mailId;
664 + $this->errorNumber = 2;
665 +
666 + if ($this->report) {
667 + acym_enqueueMessage($this->reportMessage, 'error');
668 + }
669 +
670 + return false;
671 + }
672 +
673 + $this->prepareContent($mailId, $options);
674 + $this->prepareHeaders($mailId, $receiver->id, $options);
675 + $this->prepareAddresses($mailId, $receiver);
676 + $this->prepareBcc($mailId, $options);
677 + $this->prepareAttachments($mailId, $options);
678 + $this->replaceDynamicContent($mailId);
679 + $this->prepareTracking($mailId, $receiver);
680 + if (!$this->handleDtexts($receiver)) {
681 + $this->dtextsFailed = true;
682 +
683 + return false;
684 + }
685 +
686 + $status = $this->send();
687 +
688 + $this->initStatistics($status, $receiver->id);
689 +
690 + return $status;
691 + }
692 +
693 + public function triggerFollowUpAgain(string $mailId, string $userId): void
694 + {
695 + if ($this->defaultMail[$mailId]->type !== MailClass::TYPE_FOLLOWUP) {
696 + return;
697 + }
698 +
699 + $followUpClass = new FollowupClass();
700 + $followUp = $followUpClass->getOneByMailId(intval($mailId));
701 +
702 + if (empty($followUp)) {
703 + return;
704 + }
705 +
706 + if (empty($followUp->loop)) {
707 + return;
708 + }
709 +
710 + $lastEmail = $followUpClass->getLastEmail(intval($followUp->id));
711 + if (empty($lastEmail) || $lastEmail->id != $mailId) {
712 + return;
713 + }
714 +
715 + $delay = 0;
716 + if (!empty($followUp->loop_delay)) {
717 + $delay = $followUp->loop_delay;
718 + }
719 +
720 + $mailToSkip = [];
721 + if (!empty($followUp->loop_mail_skip)) {
722 + $mailToSkip = json_decode($followUp->loop_mail_skip);
723 + }
724 +
725 + $followUpClass->triggerFollowUp(intval($followUp->id), intval($userId), $delay, $mailToSkip);
726 + }
727 +
728 + public function statClick(int $mailId, int $userid, bool $fromStat = false, string $userkey = ''): void
729 + {
730 + if (!$fromStat && !in_array($this->type, MailClass::TYPES_WITH_STATS)) {
731 + return;
732 + }
733 +
734 + $urlClass = new UrlClass();
735 + $urls = [];
736 +
737 + $trackingSystemExternalWebsite = $this->config->get('trackingsystemexternalwebsite', 1);
738 + $trackingSystem = $this->config->get('trackingsystem', 'acymailing');
739 + if (false === strpos($trackingSystem, 'acymailing') && false === strpos($trackingSystem, 'google')) {
740 + return;
741 + }
742 +
743 + if (strpos($trackingSystem, 'google') !== false) {
744 + $mail = $this->mailClass->getOneById($mailId);
745 + $campaignClass = new CampaignClass();
746 + $campaign = $campaignClass->getOneCampaignByMailId($mailId);
747 +
748 + $utmCampaign = acym_getAlias($mail->subject);
749 + }
750 +
751 + preg_match_all(self::REGEX_URL, $this->body, $results);
752 + if (empty($results)) {
753 + return;
754 + }
755 +
756 + $countLinks = array_count_values($results[1]);
757 + if (array_product($countLinks) != 1) {
758 + $previousLinkHandled = '';
759 + foreach ($results[1] as $key => $url) {
760 + if ($countLinks[$url] === 1) {
761 + continue;
762 + }
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
765 + $previousIsOutlook = false;
766 + if (strpos($results[0][$key], '<v:roundrect') === 0) {
767 + $previousLinkHandled = $results[0][$key];
768 + if ($countLinks[$url] === 2) {
769 + $countLinks[$url] = 1;
770 + continue;
771 + }
772 + } elseif (strpos($previousLinkHandled, '<v:roundrect') === 0) {
773 + $previousIsOutlook = true;
774 + }
775 + $previousLinkHandled = $results[0][$key];
776 +
777 + if (!$previousIsOutlook) {
778 + $countLinks[$url]--;
779 + }
780 +
781 + $toAddUrl = (strpos($url, '?') === false ? '?' : '&').'idU='.$countLinks[$url];
782 +
783 + if ($previousIsOutlook) {
784 + $countLinks[$url]--;
785 + }
786 +
787 + $posHash = strpos($url, '#');
788 + if ($posHash !== false) {
789 + $newURL = substr($url, 0, $posHash).$toAddUrl.substr($url, $posHash);
790 + } else {
791 + $newURL = $url.$toAddUrl;
792 + }
793 +
794 + $this->body = preg_replace('#href="('.preg_quote($url, '#').')"#Uis', 'href="'.$newURL.'"', $this->body, 1);
795 +
796 + $results[0][$key] = 'href="'.$newURL.'"';
797 + $results[1][$key] = $newURL;
798 + }
799 + }
800 +
801 + foreach ($results[1] as $i => $url) {
802 + $urlsNotToTrack = [
803 + 'task=unsub',
804 + 'task=disableTracking',
805 + 'fonts.googleapis.com',
806 + ];
807 +
808 + $track = true;
809 +
810 + foreach ($urlsNotToTrack as $urlNotToTrack) {
811 + if (strpos($url, $urlNotToTrack)) {
812 + $track = false;
813 + break;
814 + }
815 + }
816 +
817 + //We don't track unsubscribe link
818 + if (isset($urls[$results[0][$i]]) || !$track) {
819 + continue;
820 + }
821 +
822 + //We often need to check if the url is within the website... but we don't care about http or https
823 + $simplifiedUrl = str_replace(['https://', 'http://', 'www.'], '', $url);
824 + $simplifiedWebsite = str_replace(['https://', 'http://', 'www.'], '', ACYM_LIVE);
825 + $internalUrl = strpos($simplifiedUrl, rtrim($simplifiedWebsite, '/')) === 0;
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
829 + $subfolder = false;
830 + if ($internalUrl) {
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
834 + if (strpos($urlWithoutBase, '/') || strpos($urlWithoutBase, '?')) {
835 + // Get the supposed sub-folder name
836 + $slashPosition = strpos($urlWithoutBase, '/');
837 + $folderName = substr($urlWithoutBase, 0, !$slashPosition ? strpos($urlWithoutBase, '?') : $slashPosition);
838 + //There is no dot in a folder!
839 + if (strpos($folderName, '.') === false) {
840 + $subfolder = @is_dir(ACYM_ROOT.$folderName);
841 + }
842 + }
843 + }
844 +
845 + if ((!$internalUrl || $subfolder) && $trackingSystemExternalWebsite != 1) {
846 + continue;
847 + }
848 +
849 + if (strpos($url, 'utm_source') === false && strpos($trackingSystem, 'google') !== false) {
850 + $idToUse = empty($campaign) ? $mailId : $campaign->id;
851 + $args = [];
852 + $args[] = empty($campaign->sending_params['utm_source']) ? 'utm_source=newsletter_'.$idToUse : 'utm_source='.urlencode($campaign->sending_params['utm_source']);
853 + $args[] = empty($campaign->sending_params['utm_medium']) ? 'utm_medium=email' : 'utm_medium='.urlencode($campaign->sending_params['utm_medium']);
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
856 + $anchor = '';
857 + if (strpos($url, '#') !== false) {
858 + $anchor = substr($url, strpos($url, '#'));
859 + $url = substr($url, 0, strpos($url, '#'));
860 + }
861 +
862 + if (strpos($url, '?')) {
863 + $mytracker = $url.'&'.implode('&', $args);
864 + } else {
865 + $mytracker = $url.'?'.implode('&', $args);
866 + }
867 + //We add back the anchor if we had one
868 + $mytracker .= $anchor;
869 + $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
870 +
871 + //Set the url variable so that we can use it later on...
872 + $url = $mytracker;
873 + }
874 +
875 + if (strpos($trackingSystem, 'acymailing') !== false) {
876 + $isAutologin = strpos($url, self::AUTOLOGIN_PARAMS) !== false;
877 + if ($isAutologin) {
878 + $url = $this->stripAutologinParams($url);
879 + }
880 +
881 + if (preg_match('#passw|modify#i', $url)) {
882 + continue;
883 + }
884 +
885 + $urlForRegistration = $url;
886 +
887 + if (!$fromStat) {
888 + $mytracker = $urlClass->getUrl($urlForRegistration, $mailId, $userid, $userkey);
889 + }
890 +
891 + if (empty($mytracker)) {
892 + continue;
893 + }
894 +
895 + if ($isAutologin) {
896 + $separator = strpos($mytracker, '?') === false ? '?' : '&amp;';
897 + $mytracker .= $separator.self::AUTOLOGIN_PARAMS;
898 + }
899 +
900 + $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
901 + }
902 + }
903 +
904 + $this->body = str_replace(array_keys($urls), $urls, $this->body);
905 + }
906 +
907 + public function textVersion(string $html, bool $fullConvert = true): string
908 + {
909 + //Replace relative links into absolute before replacing the text version so that we keep correct urls
910 + $html = acym_absoluteURL($html);
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
914 + if ($fullConvert) {
915 + //As in HTML multiple spaces are interpreted as only one space, we remove the multiple spaces for the text version
916 + $html = preg_replace('# +#', ' ', $html);
917 + //Same thing, return chars don't exist in html so we can simply remove them, neither \t
918 + $html = str_replace(["\n", "\r", "\t"], '', $html);
919 + }
920 +
921 + $removepictureslinks = "#< *a[^>]*> *< *img[^>]*> *< *\/ *a *>#isU";
922 + $removeScript = "#< *script(?:(?!< */ *script *>).)*< */ *script *>#isU";
923 + $removeStyle = "#< *style(?:(?!< */ *style *>).)*< */ *style *>#isU";
924 + $removeStrikeTags = '#< *strike(?:(?!< */ *strike *>).)*< */ *strike *>#iU';
925 + $replaceByTwoReturnChar = '#< *(h1|h2)[^>]*>#Ui';
926 + $replaceByStars = '#< *li[^>]*>#Ui';
927 + $replaceByReturnChar1 = '#< */ *(li|td|dt|tr|div|p)[^>]*> *< *(li|td|dt|tr|div|p)[^>]*>#Ui';
928 + $replaceByReturnChar = '#< */? *(br|p|h1|h2|legend|h3|li|ul|dd|dt|h4|h5|h6|tr|td|div)[^>]*>#Ui';
929 + $replaceLinks = '/< *a[^>]*href *= *"([^#][^"]*)"[^>]*>(.+)< *\/ *a *>/Uis';
930 +
931 + $text = preg_replace(
932 + [
933 + $removepictureslinks,
934 + $removeScript,
935 + $removeStyle,
936 + $removeStrikeTags,
937 + $replaceByTwoReturnChar,
938 + $replaceByStars,
939 + $replaceByReturnChar1,
940 + $replaceByReturnChar,
941 + $replaceLinks,
942 + ],
943 + ['', '', '', '', "\n\n", "\n* ", "\n", "\n", '${2} ( ${1} )'],
944 + $html
945 + );
946 +
947 + //The striptags function may not do the job properly in some cases...
948 + $text = preg_replace('#(&lt;|&\#60;)([^ \n\r\t])#i', '&lt; ${2}', $text);
949 +
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 :) :) :)
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.
955 + $text = trim(@html_entity_decode($text, ENT_QUOTES, 'UTF-8'));
956 +
957 + if ($fullConvert) {
958 + //We do that one more time as some extra spaces may have appeared
959 + $text = preg_replace('# +#', ' ', $text);
960 + $text = preg_replace('#\n *\n\s+#', "\n\n", $text);
961 + }
962 +
963 + return $text;
964 + }
965 +
966 + /**
967 + * @throws Exception
968 + */
969 + protected function embedImages(): bool
970 + {
971 + preg_match_all('/(src|background)=[\'|"]([^"\']*)[\'|"]/Ui', $this->Body, $images);
972 +
973 + if (empty($images[2])) {
974 + return true;
975 + }
976 +
977 + $embedSuccess = true;
978 +
979 + $mimetypes = [
980 + 'bmp' => 'image/bmp',
981 + 'gif' => 'image/gif',
982 + 'jpeg' => 'image/jpeg',
983 + 'jpg' => 'image/jpeg',
984 + 'jpe' => 'image/jpeg',
985 + 'png' => 'image/png',
986 + 'tiff' => 'image/tiff',
987 + 'tif' => 'image/tiff',
988 + ];
989 +
990 + $allImages = [];
991 +
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
995 + if (isset($allImages[$url])) {
996 + continue;
997 + }
998 +
999 + // Don't embed images with a controller, most likely the stats picture
1000 + if (strpos($url, 'ctrl=') !== false) {
1001 + continue;
1002 + }
1003 +
1004 + $allImages[$url] = 1;
1005 +
1006 + // We convert the url into local directory
1007 + $path = acym_internalUrlToPath($url);
1008 + $path = $this->removeAdditionalParams($path);
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 +
1021 + $filename = str_replace(['%', ' '], '_', basename($url));
1022 + $filename = $this->removeAdditionalParams($filename);
1023 +
1024 + $md5 = md5($filename);
1025 + $cid = 'cid:'.$md5;
1026 +
1027 + // We only change the url if we were able to embed the image.
1028 + if ($this->addEmbeddedImage($path, $md5, $filename, 'base64', $mimetypes[$ext])) {
1029 + $this->Body = preg_replace('/'.preg_quote($images[0][$i], '/').'/Ui', $images[1][$i].'="'.$cid.'"', $this->Body);
1030 + } else {
1031 + $embedSuccess = false;
1032 + }
1033 + }
1034 +
1035 + return $embedSuccess;
1036 + }
1037 +
1038 + private function getEmbeddableImagePath(string $path): string
1039 + {
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 + }
1044 +
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;
1062 + }
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 + */
1072 + public function addParam(string $name, $value): void
1073 + {
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) {
1183 + return;
1184 + }
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;
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);
1329 + } else {
1330 + $this->Sender = $this->cleanText($this->config->get('bounce_email'));
1331 + }
1332 + }
1333 +
1334 + /**
1335 + * @throws Exception
1336 + */
1337 + private function prepareBcc(int $mailId, array $options): void
1338 + {
1339 + $bccAddresses = [];
1340 +
1341 + if (!empty($options['bcc'])) {
1342 + $bccAddresses = $options['bcc'];
1343 + }
1344 +
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 + }
1349 +
1350 + if (!empty($bccAddresses)) {
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 +
1357 + foreach ($bccAddresses as $oneBccAddress) {
1358 + $this->loadUser($oneBccAddress, true);
1359 + $this->addBCC($oneBccAddress);
1360 + }
1361 +
1362 + $this->receiverEmail = $receiverEmail;
1363 + $this->userLanguage = $userLanguage;
1364 + }
1365 + }
1366 +
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 + }
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 + }
1399 + }
1400 + }
1401 +
1402 + private function replaceDynamicContent(int $mailId): void
1403 + {
1404 + $this->replaceParams();
1405 +
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 + }
1442 + }
1443 + }
1444 +
1445 + private function handleDtexts(object $receiver): bool
1446 + {
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;
1454 + }
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 + }
1467 + }
1468 +
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;
1479 + }
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;
1490 + }
1491 +
1492 + private function initStatistics(bool $status, int $receiverId): void
1493 + {
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 + }
1502 + }
1503 +
1504 + private function trackingSales(int $mailId, int $userId): void
1505 + {
1506 + preg_match_all('#href[ ]*=[ ]*"(?!mailto:|\#|ymsgr:|callto:|file:|ftp:|webcal:|skype:|tel:)([^"]+)"#Ui', $this->body, $results);
1507 + if (empty($results)) {
1508 + return;
1509 + }
1510 +
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;
1515 +
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);
1526 + }
1527 + }
1528 +
1529 + private function statPicture(int $mailId, int $userId): void
1530 + {
1531 + $pictureLink = acym_frontendLink('frontstats&task=openStats&id='.$mailId.'&userid='.$userId, true, false);
1532 +
1533 + $statPicture = '<img class="spict" alt="" src="'.$pictureLink.'" border="0" width="50" height="1" />';
1534 +
1535 + if (strpos($this->body, '</body>')) {
1536 + $this->body = str_replace('</body>', $statPicture.'</body>', $this->body);
1537 + } else {
1538 + $this->body .= $statPicture;
1539 + }
1540 + }
1541 +
1542 + private function removeAdditionalParams(string $url): string
1543 + {
1544 + $additionalParamsPos = strpos($url, '?');
1545 + if (!empty($additionalParamsPos)) {
1546 + $url = substr($url, 0, $additionalParamsPos);
1547 + }
1548 +
1549 + return $url;
1550 + }
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 +
1603 + private function sendOverride(object $override, array $options): bool
1604 + {
1605 + // 2 - Prepare the email and params
1606 + for ($i = 1; $i < count($override->parameters); $i++) {
1607 + $oneParam = $override->parameters[$i];
1608 +
1609 + // Joomla emails have links as text, convert them
1610 + $unmodified = $oneParam;
1611 + $oneParam = preg_replace(
1612 + '/(http|https):\/\/(.*)/',
1613 + '<a href="$1://$2" target="_blank">$1://$2</a>',
1614 + $oneParam,
1615 + -1,
1616 + $count
1617 + );
1618 + if ($count > 0) {
1619 + $this->addParam('link'.$i, $unmodified);
1620 + }
1621 + $this->addParam('param'.$i, $oneParam);
1622 + }
1623 +
1624 + $this->addParam('subject', $options['subject']);
1625 +
1626 + // 3 - Send the email
1627 + $this->overrideEmailToSend = $override;
1628 + $statusSend = $this->sendOne($override->id, $options['to'], $options);
1629 + if (!$statusSend && !empty($this->reportMessage)) {
1630 + // Something went wrong when trying to send the override, log the information in the cron logs file
1631 + $cronHelper = new CronHelper();
1632 + $cronHelper->saveReport([$this->reportMessage]);
1633 + }
1634 +
1635 + return $statusSend;
1636 + }
1637 +
1638 + public function getSendSettings(string $type, int $mailId = 0): string
1639 + {
1640 + if (!in_array($type, ['from_name', 'from_email', 'replyto_name', 'replyto_email', 'bounce_email'])) {
1641 + return '';
1642 + }
1643 +
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 + }
1648 +
1649 + $lang = empty($this->userLanguage) ? acym_getDefaultNewsletterLanguage() : $this->userLanguage;
1650 + $setting = $this->config->get($type);
1651 + $translation = $this->config->get('sender_info_translation');
1652 +
1653 + if (!empty($translation)) {
1654 + $translation = json_decode($translation, true);
1655 +
1656 + if (!empty($translation[$lang][$type])) {
1657 + $setting = $translation[$lang][$type];
1658 + }
1659 + }
1660 +
1661 + return $setting;
1662 + }
1663 +
1664 + private function prepareEmailContent(int $mailId = 0): void
1665 + {
1666 + $this->handleRelativeURLs($mailId);
1667 + $this->inlineCSS($mailId);
1668 + $this->finalizeHtmlStructure($mailId);
1669 + }
1670 +
1671 + private function handleRelativeURLs(int $mailId): void
1672 + {
1673 + $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1674 + $mail->body = acym_absoluteURL($mail->body);
1675 + }
1676 +
1677 + private function inlineCSS(int $mailId): void
1678 + {
1679 + $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1680 + if (empty($mail->body)) {
1681 + return;
1682 + }
1683 +
1684 + global $acymailingEmogrifiedMediaCSS;
1685 + $acymailingEmogrifiedMediaCSS = '';
1686 +
1687 + $style = $this->getEmailStylesheet($mail);
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));
1691 + $domDocument = $cssInliner->getDomDocument();
1692 +
1693 + // Remove the TinyMCE ids and the trailing ; in styles
1694 + $mail->body = preg_replace(
1695 + [
1696 + '# id="mce_\d+"#Ui',
1697 + '#(style="[^"]+);"#Ui',
1698 + ],
1699 + [
1700 + '',
1701 + '$1"',
1702 + ],
1703 + HtmlPruner::fromDomDocument($domDocument)
1704 + ->removeRedundantClassesAfterCssInlined($cssInliner)
1705 + ->renderBodyContent()
1706 + );
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
1709 + $mail->body = str_replace('&zwj;', '', $mail->body);
1710 +
1711 + // Remove the extra spaces in the style attributes
1712 + $mail->body = preg_replace_callback(
1713 + '#style="([^"]+)"#Ui',
1714 + function ($matches) {
1715 + return 'style="'.str_replace(['; ', ': '], [';', ':'], $matches[1]).'"';
1716 + },
1717 + $mail->body
1718 + );
1719 + }
1720 +
1721 + private function finalizeHtmlStructure(int $mailId): void
1722 + {
1723 + $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1724 +
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">';
1726 + $finalContent .= '<head>';
1727 + $finalContent .= '<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->';
1728 + $finalContent .= '<meta http-equiv="Content-Type" content="text/html; charset='.strtolower($this->config->get('charset')).'" />'."\n";
1729 + $finalContent .= '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'."\n";
1730 + $finalContent .= '<title>'.$mail->subject.'</title>'."\n";
1731 +
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>';
1737 + $finalContent .= '<!--[if mso]><style type="text/css">#acym__wysid__template center > table { width: 580px; }</style><![endif]-->';
1738 + $finalContent .= '<!--[if !mso]><!--><style>#acym__wysid__template center > table { width: 100%; }</style><!--<![endif]-->';
1739 +
1740 + if (!empty($mail->headers)) {
1741 + $finalContent .= $mail->headers;
1742 + }
1743 + $finalContent .= '</head>';
1744 +
1745 + preg_match('@<[^>"t]*body[^>]*>@', $mail->body, $matches);
1746 + if (empty($matches[0])) {
1747 + $mail->body = '<body>'.$mail->body.'</body>';
1748 + } else {
1749 + preg_match('@<[^>"t]*/body[^>]*>@', $mail->body, $matches);
1750 + if (empty($matches[0])) {
1751 + $mail->body .= '</body>';
1752 + }
1753 + }
1754 +
1755 + $finalContent .= $mail->body;
1756 + $finalContent .= '</html>';
1757 +
1758 + $mail->body = $finalContent;
1759 + }
1760 +
1761 + private function getEmailStylesheet(object $mail): array
1762 + {
1763 + $style = [];
1764 +
1765 + // If this is a drag and drop mail we add foundation css for email
1766 + if (strpos($mail->body, 'acym__wysid__template') !== false) {
1767 + static $foundationCSS = null;
1768 + if (empty($foundationCSS)) {
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
1771 + $foundationCSS = str_replace('#acym__wysid__template ', '', $foundationCSS);
1772 + }
1773 + $style['foundation'] = $foundationCSS;
1774 + }
1775 +
1776 + static $emailFixes = null;
1777 + if (empty($emailFixes)) {
1778 + $emailFixes = acym_getEmailCssFixes();
1779 + }
1780 + $style['fixes'] = $emailFixes;
1781 +
1782 + if (!empty($mail->stylesheet)) {
1783 + $style['custom_stylesheet'] = $mail->stylesheet;
1784 + }
1785 +
1786 + $settingsStyles = $this->editorHelper->getSettingsStyle($mail->settings);
1787 + if (!empty($settingsStyles)) {
1788 + $style['settings_stylesheet'] = $settingsStyles;
1789 + }
1790 +
1791 + return $style;
1792 + }
1793 +
1794 + private function overrideAllEmails(array $options): bool
1795 + {
1796 + $this->trackEmail = false;
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 +
1853 + public function setFrom($email, $name = '', $auto = false)
1854 + {
1855 + if (!empty($email)) {
1856 + $this->From = $this->cleanText($email);
1857 + }
1858 + if (!empty($name) && $this->config->get('add_names', true)) {
1859 + $this->FromName = $this->cleanText($name);
1860 + }
1861 + }
1862 +
1863 + /**
1864 + * Outputs debugging info via user-defined method
1865 + *
1866 + * @param string $str
1867 + */
1868 + protected function edebug($str)
1869 + {
1870 + if (strpos($this->ErrorInfo, $str) === false) {
1871 + $this->ErrorInfo .= ' '.$str;
1872 + }
1873 + }
1874 +
1875 + public function getMailMIME()
1876 + {
1877 + $result = parent::getMailMIME();
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
1880 + $result = rtrim($result, static::$LE);
1881 +
1882 + if ($this->Mailer != 'mail') {
1883 + $result .= static::$LE.static::$LE;
1884 + }
1885 +
1886 + return $result;
1887 + }
1888 +
1889 + public static function validateAddress($address, $patternselect = null)
1890 + {
1891 + return true;
1892 + }
1893 +}