PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.5
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.5
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
acymailing / back / Helpers / MailerHelper.php

MailerHelper.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 11.0.5, at back/Helpers/MailerHelper.php

1,891 lines 71.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 $this->mail = clone $this->defaultMail[$mailId];
618 acym_trigger('replaceContent', [&$this->defaultMail[$mailId], true]);
619
620 $this->prepareEmailContent($mailId);
621 if (!$this->isTest) {
622 $this->storeArchiveVersion($mailId);
623 }
624
625 return $this->defaultMail[$mailId];
626 }
627
628 /**
629 * @param int|string $mailId The mail Id
630 * @param int|string|object $user Can be the user Id, an email address or the user object
631 * @param array $options Additional options for bcc, attachments, etc...
632 *
633 * @return bool
634 * @throws Exception
635 */
636 public function sendOne($mailId, $user, array $options = []): bool
637 {
638 $this->clearAll();
639
640 $receiver = $this->loadUser($user);
641 $this->isTest = !empty($options['isTest']);
642 $this->dtextsFailed = false;
643
644 if (empty($receiver->email)) {
645 $this->errorNumber = 4;
646 $this->reportMessage = acym_translationSprintf('ACYM_SEND_ERROR_USER', '<b><i>'.esc_html(is_object($user) ? $user->email : $user).'</i></b>');
647 if ($this->report) {
648 acym_enqueueMessage($this->reportMessage, 'error');
649 }
650
651 return false;
652 }
653
654 // Can be a string for Joomla 3 and WordPress
655 $mailId = intval($mailId);
656 $receiver->id = intval($receiver->id);
657
658 // Load the mail with global tags replaced
659 if (!$this->load($mailId, $receiver)) {
660 $this->reportMessage = 'Can not load the email with ID n°'.$mailId;
661 $this->errorNumber = 2;
662
663 if ($this->report) {
664 acym_enqueueMessage($this->reportMessage, 'error');
665 }
666
667 return false;
668 }
669
670 $this->prepareContent($mailId, $options);
671 $this->prepareHeaders($mailId, $receiver->id, $options);
672 $this->prepareAddresses($mailId, $receiver);
673 $this->prepareBcc($mailId, $options);
674 $this->prepareAttachments($mailId, $options);
675 $this->replaceDynamicContent($mailId);
676 $this->prepareTracking($mailId, $receiver);
677 if (!$this->handleDtexts($receiver)) {
678 $this->dtextsFailed = true;
679
680 return false;
681 }
682
683 $status = $this->send();
684
685 $this->initStatistics($status, $receiver->id);
686
687 return $status;
688 }
689
690 public function triggerFollowUpAgain(string $mailId, string $userId): void
691 {
692 if ($this->defaultMail[$mailId]->type !== MailClass::TYPE_FOLLOWUP) {
693 return;
694 }
695
696 $followUpClass = new FollowupClass();
697 $followUp = $followUpClass->getOneByMailId(intval($mailId));
698
699 if (empty($followUp)) {
700 return;
701 }
702
703 if (empty($followUp->loop)) {
704 return;
705 }
706
707 $lastEmail = $followUpClass->getLastEmail(intval($followUp->id));
708 if (empty($lastEmail) || $lastEmail->id != $mailId) {
709 return;
710 }
711
712 $delay = 0;
713 if (!empty($followUp->loop_delay)) {
714 $delay = $followUp->loop_delay;
715 }
716
717 $mailToSkip = [];
718 if (!empty($followUp->loop_mail_skip)) {
719 $mailToSkip = json_decode($followUp->loop_mail_skip);
720 }
721
722 $followUpClass->triggerFollowUp(intval($followUp->id), intval($userId), $delay, $mailToSkip);
723 }
724
725 public function statClick(int $mailId, int $userid, bool $fromStat = false, string $userkey = ''): void
726 {
727 if (!$fromStat && !in_array($this->type, MailClass::TYPES_WITH_STATS)) {
728 return;
729 }
730
731 $urlClass = new UrlClass();
732 $urls = [];
733
734 $trackingSystemExternalWebsite = $this->config->get('trackingsystemexternalwebsite', 1);
735 $trackingSystem = $this->config->get('trackingsystem', 'acymailing');
736 if (false === strpos($trackingSystem, 'acymailing') && false === strpos($trackingSystem, 'google')) {
737 return;
738 }
739
740 if (strpos($trackingSystem, 'google') !== false) {
741 $mail = $this->mailClass->getOneById($mailId);
742 $campaignClass = new CampaignClass();
743 $campaign = $campaignClass->getOneCampaignByMailId($mailId);
744
745 $utmCampaign = acym_getAlias($mail->subject);
746 }
747
748 preg_match_all(self::REGEX_URL, $this->body, $results);
749 if (empty($results)) {
750 return;
751 }
752
753 $countLinks = array_count_values($results[1]);
754 if (array_product($countLinks) != 1) {
755 $previousLinkHandled = '';
756 foreach ($results[1] as $key => $url) {
757 if ($countLinks[$url] === 1) {
758 continue;
759 }
760
761 // Handle the Outlook buttons that double the link, we need to consider it as the same link that's on the main button element
762 $previousIsOutlook = false;
763 if (strpos($results[0][$key], '<v:roundrect') === 0) {
764 $previousLinkHandled = $results[0][$key];
765 if ($countLinks[$url] === 2) {
766 $countLinks[$url] = 1;
767 continue;
768 }
769 } elseif (strpos($previousLinkHandled, '<v:roundrect') === 0) {
770 $previousIsOutlook = true;
771 }
772 $previousLinkHandled = $results[0][$key];
773
774 if (!$previousIsOutlook) {
775 $countLinks[$url]--;
776 }
777
778 $toAddUrl = (strpos($url, '?') === false ? '?' : '&').'idU='.$countLinks[$url];
779
780 if ($previousIsOutlook) {
781 $countLinks[$url]--;
782 }
783
784 $posHash = strpos($url, '#');
785 if ($posHash !== false) {
786 $newURL = substr($url, 0, $posHash).$toAddUrl.substr($url, $posHash);
787 } else {
788 $newURL = $url.$toAddUrl;
789 }
790
791 $this->body = preg_replace('#href="('.preg_quote($url, '#').')"#Uis', 'href="'.$newURL.'"', $this->body, 1);
792
793 $results[0][$key] = 'href="'.$newURL.'"';
794 $results[1][$key] = $newURL;
795 }
796 }
797
798 foreach ($results[1] as $i => $url) {
799 $urlsNotToTrack = [
800 'task=unsub',
801 'task=disableTracking',
802 'fonts.googleapis.com',
803 ];
804
805 $track = true;
806
807 foreach ($urlsNotToTrack as $urlNotToTrack) {
808 if (strpos($url, $urlNotToTrack)) {
809 $track = false;
810 break;
811 }
812 }
813
814 //We don't track unsubscribe link
815 if (isset($urls[$results[0][$i]]) || !$track) {
816 continue;
817 }
818
819 //We often need to check if the url is within the website... but we don't care about http or https
820 $simplifiedUrl = str_replace(['https://', 'http://', 'www.'], '', $url);
821 $simplifiedWebsite = str_replace(['https://', 'http://', 'www.'], '', ACYM_LIVE);
822 $internalUrl = strpos($simplifiedUrl, rtrim($simplifiedWebsite, '/')) === 0;
823
824 // If this is an internal url
825 //$subfolder : Record if the subfolder exists or not in which case it will be an external link
826 $subfolder = false;
827 if ($internalUrl) {
828 $urlWithoutBase = str_replace($simplifiedWebsite, '', $simplifiedUrl);
829 // If there is a /, it means there could be a sub-folder
830 //It can be separated with a ? as well like administrator?option=com_content
831 if (strpos($urlWithoutBase, '/') || strpos($urlWithoutBase, '?')) {
832 // Get the supposed sub-folder name
833 $slashPosition = strpos($urlWithoutBase, '/');
834 $folderName = substr($urlWithoutBase, 0, !$slashPosition ? strpos($urlWithoutBase, '?') : $slashPosition);
835 //There is no dot in a folder!
836 if (strpos($folderName, '.') === false) {
837 $subfolder = @is_dir(ACYM_ROOT.$folderName);
838 }
839 }
840 }
841
842 if ((!$internalUrl || $subfolder) && $trackingSystemExternalWebsite != 1) {
843 continue;
844 }
845
846 if (strpos($url, 'utm_source') === false && strpos($trackingSystem, 'google') !== false) {
847 $idToUse = empty($campaign) ? $mailId : $campaign->id;
848 $args = [];
849 $args[] = empty($campaign->sending_params['utm_source']) ? 'utm_source=newsletter_'.$idToUse : 'utm_source='.urlencode($campaign->sending_params['utm_source']);
850 $args[] = empty($campaign->sending_params['utm_medium']) ? 'utm_medium=email' : 'utm_medium='.urlencode($campaign->sending_params['utm_medium']);
851 $args[] = empty($campaign->sending_params['utm_campaign']) ? 'utm_campaign='.$utmCampaign : 'utm_campaign='.urlencode($campaign->sending_params['utm_campaign']);
852 //If we have an anchor we need to remove it and add it to the end of the url
853 $anchor = '';
854 if (strpos($url, '#') !== false) {
855 $anchor = substr($url, strpos($url, '#'));
856 $url = substr($url, 0, strpos($url, '#'));
857 }
858
859 if (strpos($url, '?')) {
860 $mytracker = $url.'&'.implode('&', $args);
861 } else {
862 $mytracker = $url.'?'.implode('&', $args);
863 }
864 //We add back the anchor if we had one
865 $mytracker .= $anchor;
866 $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
867
868 //Set the url variable so that we can use it later on...
869 $url = $mytracker;
870 }
871
872 if (strpos($trackingSystem, 'acymailing') !== false) {
873 $isAutologin = strpos($url, self::AUTOLOGIN_PARAMS) !== false;
874 if ($isAutologin) {
875 $url = $this->stripAutologinParams($url);
876 }
877
878 if (preg_match('#passw|modify#i', $url)) {
879 continue;
880 }
881
882 $urlForRegistration = $url;
883
884 if (!$fromStat) {
885 $mytracker = $urlClass->getUrl($urlForRegistration, $mailId, $userid, $userkey);
886 }
887
888 if (empty($mytracker)) {
889 continue;
890 }
891
892 if ($isAutologin) {
893 $separator = strpos($mytracker, '?') === false ? '?' : '&amp;';
894 $mytracker .= $separator.self::AUTOLOGIN_PARAMS;
895 }
896
897 $urls[$results[0][$i]] = str_replace($results[1][$i], $mytracker, $results[0][$i]);
898 }
899 }
900
901 $this->body = str_replace(array_keys($urls), $urls, $this->body);
902 }
903
904 public function textVersion(string $html, bool $fullConvert = true): string
905 {
906 //Replace relative links into absolute before replacing the text version so that we keep correct urls
907 $html = acym_absoluteURL($html);
908
909 //If we come from a text version, we don't want to replace the spaces.
910 //We will only do that if we come from an HTML Version to avoid breaking the user code
911 if ($fullConvert) {
912 //As in HTML multiple spaces are interpreted as only one space, we remove the multiple spaces for the text version
913 $html = preg_replace('# +#', ' ', $html);
914 //Same thing, return chars don't exist in html so we can simply remove them, neither \t
915 $html = str_replace(["\n", "\r", "\t"], '', $html);
916 }
917
918 $removepictureslinks = "#< *a[^>]*> *< *img[^>]*> *< *\/ *a *>#isU";
919 $removeScript = "#< *script(?:(?!< */ *script *>).)*< */ *script *>#isU";
920 $removeStyle = "#< *style(?:(?!< */ *style *>).)*< */ *style *>#isU";
921 $removeStrikeTags = '#< *strike(?:(?!< */ *strike *>).)*< */ *strike *>#iU';
922 $replaceByTwoReturnChar = '#< *(h1|h2)[^>]*>#Ui';
923 $replaceByStars = '#< *li[^>]*>#Ui';
924 $replaceByReturnChar1 = '#< */ *(li|td|dt|tr|div|p)[^>]*> *< *(li|td|dt|tr|div|p)[^>]*>#Ui';
925 $replaceByReturnChar = '#< */? *(br|p|h1|h2|legend|h3|li|ul|dd|dt|h4|h5|h6|tr|td|div)[^>]*>#Ui';
926 $replaceLinks = '/< *a[^>]*href *= *"([^#][^"]*)"[^>]*>(.+)< *\/ *a *>/Uis';
927
928 $text = preg_replace(
929 [
930 $removepictureslinks,
931 $removeScript,
932 $removeStyle,
933 $removeStrikeTags,
934 $replaceByTwoReturnChar,
935 $replaceByStars,
936 $replaceByReturnChar1,
937 $replaceByReturnChar,
938 $replaceLinks,
939 ],
940 ['', '', '', '', "\n\n", "\n* ", "\n", "\n", '${2} ( ${1} )'],
941 $html
942 );
943
944 //The striptags function may not do the job properly in some cases...
945 $text = preg_replace('#(&lt;|&\#60;)([^ \n\r\t])#i', '&lt; ${2}', $text);
946
947 //BE CAREFUL!!!! This space is not a space, it's a ALT0160!! which means &nbsp;
948 $text = str_replace([" ", "&nbsp;"], ' ', acym_stripTags($text));
949 //BE CAREFUL!! That is magic code :) :) :)
950
951 //@ is added on the call of html_entity_decode because on PHP 4, warnings are displayed using this function with utf-8 characters.
952 $text = trim(@html_entity_decode($text, ENT_QUOTES, 'UTF-8'));
953
954 if ($fullConvert) {
955 //We do that one more time as some extra spaces may have appeared
956 $text = preg_replace('# +#', ' ', $text);
957 $text = preg_replace('#\n *\n\s+#', "\n\n", $text);
958 }
959
960 return $text;
961 }
962
963 /**
964 * @throws Exception
965 */
966 protected function embedImages(): bool
967 {
968 preg_match_all('/(src|background)=[\'|"]([^"\']*)[\'|"]/Ui', $this->Body, $images);
969
970 if (empty($images[2])) {
971 return true;
972 }
973
974 $embedSuccess = true;
975
976 $mimetypes = [
977 'bmp' => 'image/bmp',
978 'gif' => 'image/gif',
979 'jpeg' => 'image/jpeg',
980 'jpg' => 'image/jpeg',
981 'jpe' => 'image/jpeg',
982 'png' => 'image/png',
983 'tiff' => 'image/tiff',
984 'tif' => 'image/tiff',
985 ];
986
987 $allImages = [];
988
989 foreach ($images[2] as $i => $url) {
990 //We don't add twice the same images otherwise there is a bug
991 //and the picture is really attached but not in hidden base64
992 if (isset($allImages[$url])) {
993 continue;
994 }
995
996 // Don't embed images with a controller, most likely the stats picture
997 if (strpos($url, 'ctrl=') !== false) {
998 continue;
999 }
1000
1001 $allImages[$url] = 1;
1002
1003 // We convert the url into local directory
1004 $path = acym_internalUrlToPath($url);
1005 $path = $this->removeAdditionalParams($path);
1006
1007 // The file has to be an image located in the website's folder
1008 $path = $this->getEmbeddableImagePath($path);
1009 if (empty($path)) {
1010 continue;
1011 }
1012
1013 $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
1014 if (!isset($mimetypes[$ext])) {
1015 continue;
1016 }
1017
1018 $filename = str_replace(['%', ' '], '_', basename($url));
1019 $filename = $this->removeAdditionalParams($filename);
1020
1021 $md5 = md5($filename);
1022 $cid = 'cid:'.$md5;
1023
1024 // We only change the url if we were able to embed the image.
1025 if ($this->addEmbeddedImage($path, $md5, $filename, 'base64', $mimetypes[$ext])) {
1026 $this->Body = preg_replace('/'.preg_quote($images[0][$i], '/').'/Ui', $images[1][$i].'="'.$cid.'"', $this->Body);
1027 } else {
1028 $embedSuccess = false;
1029 }
1030 }
1031
1032 return $embedSuccess;
1033 }
1034
1035 private function getEmbeddableImagePath(string $path): string
1036 {
1037 // No url and no stream wrapper, we only read local files
1038 if (empty($path) || preg_match('#^[a-z][a-z\d+.-]*://#i', $path) === 1) {
1039 return '';
1040 }
1041
1042 // The file must be inside the website's folder
1043 $root = rtrim(ACYM_ROOT, DS).DS;
1044 if (strpos($path, $root) !== 0 || preg_match('#(^|[/\\\\])\.\.([/\\\\]|$)#', substr($path, strlen($root))) === 1) {
1045 return '';
1046 }
1047
1048 if (!is_file($path) || !is_readable($path)) {
1049 return '';
1050 }
1051
1052 // An image extension doesn't tell what the file actually contains, so we check the content itself
1053 $imageInfo = @getimagesize($path);
1054 if (empty($imageInfo['mime']) || strpos($imageInfo['mime'], 'image/') !== 0) {
1055 return '';
1056 }
1057
1058 return $path;
1059 }
1060
1061 public function cleanText(?string $text): string
1062 {
1063 return trim(preg_replace('/(%0A|%0D|\n+|\r+)/i', '', (string)$text));
1064 }
1065
1066 /**
1067 * Adds shortcodes available in the email content
1068 */
1069 public function addParam(string $name, $value): void
1070 {
1071 $tagName = '{'.$name.'}';
1072 $this->parameters[$tagName] = $value;
1073 }
1074
1075 public function overrideEmail(array $options): bool
1076 {
1077 $overrideAllEmails = $this->config->get('send_website_emails', 0) == 1;
1078
1079 $overrideClass = new OverrideClass();
1080 $override = $overrideClass->getMailByBaseContent($options['subject'], $options['message']);
1081
1082 // We let the CMS send the email
1083 if (empty($override) && !$overrideAllEmails) {
1084 return false;
1085 }
1086
1087 // AcyMailing needs to send the email
1088 $this->autoAddUser = true;
1089
1090 if (empty($override)) {
1091 $this->trackEmail = false;
1092
1093 return $this->overrideAllEmails($options);
1094 } else {
1095 $this->trackEmail = true;
1096
1097 return $this->sendOverride($override, $options);
1098 }
1099 }
1100
1101 public function isPortOpen(int $port, string $targetServer): bool
1102 {
1103 if (!function_exists('fsockopen')) {
1104 $this->portError = acym_translation('ACYM_FSOCKOPEN');
1105
1106 return false;
1107 }
1108
1109 $errorCode = 0;
1110 $errorMessage = 0;
1111 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fsockopen -- Connectivity test for SMTP ports.
1112 $fp = @fsockopen($targetServer, $port, $errorCode, $errorMessage, 5);
1113
1114 if (is_resource($fp)) {
1115 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing the socket resource opened above, not a filesystem handle.
1116 fclose($fp);
1117
1118 return true;
1119 } else {
1120 $this->portError = $errorCode.' - '.acym_utf8Encode($errorMessage);
1121
1122 return false;
1123 }
1124 }
1125
1126 private function getSendingMethodSettings(string $settingName, string $default = ''): string
1127 {
1128 if (empty($this->currentMethodSetting)) {
1129 return $this->config->get($settingName, $default);
1130 }
1131
1132 return $this->currentMethodSetting[$settingName] ?? $default;
1133 }
1134
1135 private function getSendingMethod(): string
1136 {
1137 $this->currentMethodSetting = [];
1138
1139 $sendingMethods = $this->config->get('sending_method_list', '{}');
1140 $sendingMethods = json_decode($sendingMethods, true);
1141 $sendingMethodsByList = $this->config->get('sending_method_list_by_list', '{}');
1142 $sendingMethodsByList = json_decode($sendingMethodsByList, true);
1143
1144 if (empty($this->listsIds) || empty($sendingMethods) || empty($sendingMethodsByList) || empty($sendingMethodsByList[$this->listsIds[0]])) {
1145 return $this->config->get('mailer_method', 'phpmail');
1146 }
1147
1148 $sendingMethodIds = [];
1149
1150 foreach ($this->listsIds as $listId) {
1151 if (!empty($sendingMethodsByList[$listId])) {
1152 $sendingMethodIds[] = $sendingMethodsByList[$listId];
1153 }
1154 }
1155
1156 if (count($sendingMethodIds) !== count($this->listsIds) || count(array_unique($sendingMethodIds)) !== 1) {
1157 return $this->config->get('mailer_method', 'phpmail');
1158 }
1159
1160 $sendingMethodId = $sendingMethodIds[0];
1161
1162 if (empty($sendingMethods[$sendingMethodId])) {
1163 return $this->config->get('mailer_method', 'phpmail');
1164 }
1165
1166 $this->currentMethodSetting = $sendingMethods[$sendingMethodId];
1167
1168 return $sendingMethods[$sendingMethodId]['mailer_method'];
1169 }
1170
1171 private function isExternal(string $method): void
1172 {
1173 $this->Mailer = 'external';
1174 $this->externalMailer = $method;
1175 }
1176
1177 private function storeArchiveVersion(int $mailId): void
1178 {
1179 if ($this->defaultMail[$mailId]->type !== MailClass::TYPE_STANDARD) {
1180 return;
1181 }
1182
1183 $mailArchive = new \stdClass();
1184
1185 $alreadyExisting = $this->mailArchiveClass->getOneByMailId($mailId);
1186 if (!empty($alreadyExisting)) {
1187 $mailArchive->id = $alreadyExisting->id;
1188 }
1189
1190 $mailArchive->mail_id = $mailId;
1191 $mailArchive->date = time();
1192 $mailArchive->body = $this->defaultMail[$mailId]->body;
1193 $mailArchive->subject = $this->defaultMail[$mailId]->subject;
1194 $mailArchive->settings = $this->defaultMail[$mailId]->settings;
1195 $mailArchive->stylesheet = $this->defaultMail[$mailId]->stylesheet;
1196 $mailArchive->attachments = $this->defaultMail[$mailId]->attachments;
1197 $this->mailArchiveClass->save($mailArchive);
1198 }
1199
1200 private function canTrack(int $mailId, object $user): bool
1201 {
1202 if ($this->isTest || empty($mailId) || empty($user) || !isset($user->tracking) || $user->tracking != 1) {
1203 return false;
1204 }
1205
1206 $mail = $this->mailClass->getOneById($mailId);
1207 if (!empty($mail) && $mail->tracking != 1) {
1208 return false;
1209 }
1210
1211 $lists = $this->mailClass->getAllListsByMailIdAndUserId($mailId, $user->id);
1212
1213 foreach ($lists as $list) {
1214 if ($list->tracking != 1) {
1215 return false;
1216 }
1217 }
1218
1219 return true;
1220 }
1221
1222 /**
1223 * @param mixed $user
1224 *
1225 * @throws Exception
1226 */
1227 private function loadUser($user, bool $force = false): object
1228 {
1229 if (is_numeric($user)) {
1230 $receiver = $this->userClass->getOneById($user);
1231 } elseif (is_object($user)) {
1232 $receiver = $user;
1233 } elseif (is_string($user) && strpos($user, '@')) {
1234 acym_setVar('acy_source', 'When sending a test');
1235 $this->userClass->triggers = $this->userCreationTriggers;
1236 $receiver = $this->userClass->getOneByEmail($user, $force || $this->autoAddUser);
1237 }
1238
1239 if (empty($receiver)) {
1240 throw new Exception(esc_attr(acym_translation('ACYM_USER_NOT_FOUND')));
1241 }
1242
1243 $this->userLanguage = empty($receiver->language) ? acym_getLanguageTag() : $receiver->language;
1244 $this->receiverEmail = (string)($receiver->email ?? '');
1245
1246 return $receiver;
1247 }
1248
1249 private function prepareContent(int $mailId, array $options): void
1250 {
1251 $this->isHTML();
1252
1253 if (!empty($this->defaultMail[$mailId]->stylesheet)) {
1254 $this->stylesheet = $this->defaultMail[$mailId]->stylesheet;
1255 }
1256 $this->settings = empty($this->defaultMail[$mailId]->settings) ? [] : json_decode($this->defaultMail[$mailId]->settings, true);
1257
1258 $this->Subject = $this->defaultMail[$mailId]->subject;
1259 $this->Body = $this->defaultMail[$mailId]->body;
1260 if ($this->isTest && !empty($options['testNote'])) {
1261 $this->Body = '<div style="text-align: center; padding: 25px; font-family: Poppins; font-size: 20px">'.$options['testNote'].'</div>'.$this->Body;
1262 }
1263
1264 //We add the intro text at the top of the body
1265 if (!empty($this->introtext)) {
1266 $this->Body = $this->introtext.$this->Body;
1267 }
1268
1269 if (!empty($this->defaultMail[$mailId]->preheader)) {
1270 $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;">';
1271 $preHeader .= $this->defaultMail[$mailId]->preheader;
1272 $preHeader .= '</div><!--<![endif]-->';
1273
1274 //We want to insert the preview at the start of the body so we match the start of the mail
1275 preg_match('#(<(.*)<body(.*)>)#Uis', $this->Body, $matches);
1276 if (empty($matches) || empty($matches[1])) {
1277 $this->Body = $preHeader.$this->Body;
1278 } else {
1279 $this->Body = $matches[1].$preHeader.str_replace($matches[1], '', $this->Body);
1280 }
1281 }
1282
1283 if (ACYM_CMS === 'wordpress') {
1284 ob_start();
1285 $this->Body = do_shortcode($this->Body);
1286 ob_end_clean();
1287 }
1288 }
1289
1290 private function prepareHeaders(int $mailId, int $receiverId, array $options): void
1291 {
1292 // Specify a messageID which will be kept by most mail clients and in the feedback loop
1293 $subject = base64_encode(acym_rand(0, 9999999)).'AC'.$receiverId.'Y'.$this->defaultMail[$mailId]->id.'BA'.base64_encode(time().acym_rand(0, 99999));
1294 $this->MessageID = '<'.preg_replace('|[^a-z0-9+_]|i', '', $subject).'@'.$this->serverHostname().'>';
1295 $this->addCustomHeader('Feedback-ID', $this->defaultMail[$mailId]->id.':'.$receiverId.':'.$this->defaultMail[$mailId]->type.':'.base64_encode(ACYM_ROOT));
1296
1297 if (!empty($this->defaultMail[$mailId]->headers)) {
1298 $this->mailHeader = $this->defaultMail[$mailId]->headers;
1299 }
1300
1301 if (!empty($options['headers'])) {
1302 foreach ($options['headers'] as $headerName => $headerValue) {
1303 $this->addCustomHeader(trim($headerName), trim($headerValue));
1304 }
1305 }
1306 }
1307
1308 private function prepareAddresses(int $mailId, object $receiver): void
1309 {
1310 $receiverName = '';
1311 if ($this->config->get('add_names', true)) {
1312 $receiverName = $this->cleanText($receiver->name);
1313 //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
1314 if ($receiverName == $this->cleanText($receiver->email)) {
1315 $receiverName = '';
1316 }
1317 }
1318 $this->addAddress($this->cleanText($receiver->email), $receiverName);
1319
1320 $this->setFrom($this->getSendSettings('from_email', $mailId), $this->getSendSettings('from_name', $mailId));
1321 $this->acymAddReplyTo($this->defaultMail[$mailId]->reply_to_email ?? '', $this->defaultMail[$mailId]->reply_to_name ?? '');
1322
1323 // This is the bounce email address
1324 if (!empty($this->defaultMail[$mailId]->bounce_email)) {
1325 $this->Sender = $this->cleanText($this->defaultMail[$mailId]->bounce_email);
1326 } else {
1327 $this->Sender = $this->cleanText($this->config->get('bounce_email'));
1328 }
1329 }
1330
1331 /**
1332 * @throws Exception
1333 */
1334 private function prepareBcc(int $mailId, array $options): void
1335 {
1336 $bccAddresses = [];
1337
1338 if (!empty($options['bcc'])) {
1339 $bccAddresses = $options['bcc'];
1340 }
1341
1342 if (!empty($this->defaultMail[$mailId]->bcc)) {
1343 $emailBccAddresses = explode(';', trim(str_replace([',', ' '], ';', $this->defaultMail[$mailId]->bcc)));
1344 $bccAddresses = array_unique(array_merge($bccAddresses, $emailBccAddresses));
1345 }
1346
1347 if (!empty($bccAddresses)) {
1348 $bccAddresses = array_filter($bccAddresses);
1349
1350 // loadUser() overwrites the current receiver, keep the real one
1351 $receiverEmail = $this->receiverEmail;
1352 $userLanguage = $this->userLanguage;
1353
1354 foreach ($bccAddresses as $oneBccAddress) {
1355 $this->loadUser($oneBccAddress, true);
1356 $this->addBCC($oneBccAddress);
1357 }
1358
1359 $this->receiverEmail = $receiverEmail;
1360 $this->userLanguage = $userLanguage;
1361 }
1362 }
1363
1364 private function prepareAttachments(int $mailId, array $options): void
1365 {
1366 $attachments = [];
1367
1368 if (!empty($this->defaultMail[$mailId]->attach)) {
1369 $attachments = $this->defaultMail[$mailId]->attach;
1370 }
1371
1372 if (!empty($options['attachments'])) {
1373 $attachments = array_merge($attachments, $options['attachments']);
1374 }
1375
1376 if (empty($attachments)) {
1377 return;
1378 }
1379
1380 if ($this->config->get('embed_files')) {
1381 foreach ($attachments as $attachment) {
1382 $this->addAttachment($attachment->filename, $attachment->name);
1383 }
1384 } else {
1385 $attachStringHTML = '<fieldset><legend>'.acym_translation('ACYM_ATTACHMENTS').'</legend><table>';
1386 foreach ($attachments as $attachment) {
1387 $attachStringHTML .= '<tr><td><a href="'.$attachment->url.'" target="_blank">'.$attachment->name.'</a></td></tr>';
1388 }
1389 $attachStringHTML .= '</table></fieldset>';
1390
1391 if ($this->config->get('attachments_position', 'bottom') === 'top') {
1392 $this->Body = $attachStringHTML.'<br />'.$this->Body;
1393 } else {
1394 $this->Body .= '<br />'.$attachStringHTML;
1395 }
1396 }
1397 }
1398
1399 private function replaceDynamicContent(int $mailId): void
1400 {
1401 $this->replaceParams();
1402
1403 // Left side are mail columns on which we replace the short codes
1404 $this->body = &$this->Body;
1405 $this->altbody = &$this->AltBody;
1406 $this->subject = &$this->Subject;
1407 $this->from = &$this->From;
1408 $this->fromName = &$this->FromName;
1409 $this->replyto = &$this->ReplyTo;
1410 $this->replyname = $this->defaultMail[$mailId]->reply_to_name ?? '';
1411 $this->replyemail = $this->defaultMail[$mailId]->reply_to_email ?? '';
1412 $this->mailId = intval($this->defaultMail[$mailId]->id);
1413 $this->id = $this->mailId;
1414 $this->creator_id = intval($this->defaultMail[$mailId]->creator_id);
1415 $this->type = $this->defaultMail[$mailId]->type;
1416 $this->stylesheet = &$this->stylesheet;
1417 $this->links_language = $this->defaultMail[$mailId]->links_language;
1418 }
1419
1420 private function stripAutologinParams(string $url): string
1421 {
1422 return str_replace(
1423 ['?'.self::AUTOLOGIN_PARAMS.'&amp;', '?'.self::AUTOLOGIN_PARAMS, '&amp;'.self::AUTOLOGIN_PARAMS],
1424 ['?', '', ''],
1425 $url
1426 );
1427 }
1428
1429 private function prepareTracking(int $mailId, object $receiver): void
1430 {
1431 if ($this->canTrack($mailId, $receiver)) {
1432 $receiver->id = intval($receiver->id);
1433 $this->statPicture($this->mailId, $receiver->id);
1434 $this->body = acym_absoluteURL($this->body);
1435 $this->statClick($this->mailId, $receiver->id, false, $receiver->key ?? '');
1436 if (acym_isTrackingSalesActive()) {
1437 $this->trackingSales($this->mailId, $receiver->id);
1438 }
1439 }
1440 }
1441
1442 private function handleDtexts(object $receiver): bool
1443 {
1444 $this->replaceParams();
1445
1446 // Sending a spam-test, use the current user instead
1447 if (strpos($receiver->email, '@mt.acyba.com') !== false) {
1448 $currentUser = $this->userClass->getOneByEmail(acym_currentUserEmail());
1449 if (empty($currentUser)) {
1450 $currentUser = $receiver;
1451 }
1452 $result = acym_trigger('replaceUserInformation', [&$this, &$currentUser, true]);
1453 } else {
1454 $result = acym_trigger('replaceUserInformation', [&$this, &$receiver, true]);
1455 if (!empty($result)) {
1456 foreach ($result as $oneResult) {
1457 if (!empty($oneResult) && !$oneResult['send']) {
1458 $this->reportMessage = $oneResult['message'];
1459
1460 return false;
1461 }
1462 }
1463 }
1464 }
1465
1466 global $acymLanguages;
1467 if (!empty($acymLanguages['userLanguage'])) {
1468 unset($acymLanguages['userLanguage']);
1469 }
1470
1471 // A user based dtext may have inserted HTML content, we need to re-inline the CSS to it
1472 foreach ($result as $oneResult) {
1473 if (!empty($oneResult['emogrifier'])) {
1474 $this->prepareEmailContent();
1475 break;
1476 }
1477 }
1478
1479 if ($this->config->get('multiple_part', false)) {
1480 $bodyForText = preg_replace('#<head\b[^>]*>.*?</head\s*>#is', '', $this->Body);
1481 $this->altbody = $this->textVersion($bodyForText);
1482 }
1483
1484 $this->replaceParams();
1485
1486 return true;
1487 }
1488
1489 private function initStatistics(bool $status, int $receiverId): void
1490 {
1491 if ($this->trackEmail) {
1492 $statsAdd = [];
1493 $statsAdd[$this->mailId][intval($status)][] = $receiverId;
1494
1495 $queueHelper = new QueueHelper();
1496 $queueHelper->statsAdd($statsAdd);
1497 $this->trackEmail = false;
1498 }
1499 }
1500
1501 private function trackingSales(int $mailId, int $userId): void
1502 {
1503 preg_match_all('#href[ ]*=[ ]*"(?!mailto:|\#|ymsgr:|callto:|file:|ftp:|webcal:|skype:|tel:)([^"]+)"#Ui', $this->body, $results);
1504 if (empty($results)) {
1505 return;
1506 }
1507
1508 foreach ($results[1] as $url) {
1509 $simplifiedUrl = str_replace(['https://', 'http://', 'www.'], '', $url);
1510 $simplifiedWebsite = str_replace(['https://', 'http://', 'www.'], '', ACYM_LIVE);
1511 if (strpos($simplifiedUrl, rtrim($simplifiedWebsite, '/')) === false || strpos($url, 'task=unsub')) continue;
1512
1513 $toAddUrl = (strpos($url, '?') === false ? '?' : '&').'linkReferal='.$mailId.'-'.$userId;
1514
1515 $posHash = strpos($url, '#');
1516 if ($posHash !== false) {
1517 $newURL = substr($url, 0, $posHash).$toAddUrl.substr($url, $posHash);
1518 } else {
1519 $newURL = $url.$toAddUrl;
1520 }
1521
1522 $this->body = preg_replace('#href="('.preg_quote($url, '#').')"#Uis', 'href="'.$newURL.'"', $this->body);
1523 }
1524 }
1525
1526 private function statPicture(int $mailId, int $userId): void
1527 {
1528 $pictureLink = acym_frontendLink('frontstats&task=openStats&id='.$mailId.'&userid='.$userId, true, false);
1529
1530 $statPicture = '<img class="spict" alt="" src="'.$pictureLink.'" border="0" width="50" height="1" />';
1531
1532 if (strpos($this->body, '</body>')) {
1533 $this->body = str_replace('</body>', $statPicture.'</body>', $this->body);
1534 } else {
1535 $this->body .= $statPicture;
1536 }
1537 }
1538
1539 private function removeAdditionalParams(string $url): string
1540 {
1541 $additionalParamsPos = strpos($url, '?');
1542 if (!empty($additionalParamsPos)) {
1543 $url = substr($url, 0, $additionalParamsPos);
1544 }
1545
1546 return $url;
1547 }
1548
1549 private function replaceParams(): void
1550 {
1551 if (empty($this->parameters)) {
1552 return;
1553 }
1554
1555 $pluginHelper = new PluginHelper();
1556
1557 //We create an extra tag which contains all possible parameters...
1558 $this->generateAllParams();
1559
1560 $vars = [
1561 'Subject',
1562 'Body',
1563 'From',
1564 'FromName',
1565 'replyname',
1566 'replyemail',
1567 ];
1568
1569 foreach ($vars as $oneVar) {
1570 if (!empty($this->$oneVar)) {
1571 $this->$oneVar = $pluginHelper->replaceDText($this->$oneVar, $this->parameters);
1572 }
1573 }
1574
1575 if (!empty($this->ReplyTo)) {
1576 foreach ($this->ReplyTo as $i => $replyto) {
1577 foreach ($replyto as $a => $oneval) {
1578 $this->ReplyTo[$i][$a] = $pluginHelper->replaceDText($this->ReplyTo[$i][$a], $this->parameters);
1579 }
1580 }
1581 }
1582 }
1583
1584 /**
1585 * Create a new parameter called {alltags} which will include all the others
1586 */
1587 private function generateAllParams(): void
1588 {
1589 $result = '<table style="border:1px solid;border-collapse:collapse;" border="1" cellpadding="10"><tr><td>Tag</td><td>Value</td></tr>';
1590 foreach ($this->parameters as $name => $value) {
1591 //Just in case of...
1592 if (!is_string($value)) continue;
1593
1594 $result .= '<tr><td>'.trim($name, '{}').'</td><td>'.$value.'</td></tr>';
1595 }
1596 $result .= '</table>';
1597 $this->addParam('allshortcodes', $result);
1598 }
1599
1600 private function sendOverride(object $override, array $options): bool
1601 {
1602 // 2 - Prepare the email and params
1603 for ($i = 1; $i < count($override->parameters); $i++) {
1604 $oneParam = $override->parameters[$i];
1605
1606 // Joomla emails have links as text, convert them
1607 $unmodified = $oneParam;
1608 $oneParam = preg_replace(
1609 '/(http|https):\/\/(.*)/',
1610 '<a href="$1://$2" target="_blank">$1://$2</a>',
1611 $oneParam,
1612 -1,
1613 $count
1614 );
1615 if ($count > 0) {
1616 $this->addParam('link'.$i, $unmodified);
1617 }
1618 $this->addParam('param'.$i, $oneParam);
1619 }
1620
1621 $this->addParam('subject', $options['subject']);
1622
1623 // 3 - Send the email
1624 $this->overrideEmailToSend = $override;
1625 $statusSend = $this->sendOne($override->id, $options['to'], $options);
1626 if (!$statusSend && !empty($this->reportMessage)) {
1627 // Something went wrong when trying to send the override, log the information in the cron logs file
1628 $cronHelper = new CronHelper();
1629 $cronHelper->saveReport([$this->reportMessage]);
1630 }
1631
1632 return $statusSend;
1633 }
1634
1635 public function getSendSettings(string $type, int $mailId = 0): string
1636 {
1637 if (!in_array($type, ['from_name', 'from_email', 'replyto_name', 'replyto_email', 'bounce_email'])) {
1638 return '';
1639 }
1640
1641 $mailType = strpos($type, 'replyto') !== false ? str_replace('replyto', 'reply_to', $type) : $type;
1642 if (!empty($mailId) && !empty($this->defaultMail[$mailId]->$mailType)) {
1643 return $this->defaultMail[$mailId]->$mailType;
1644 }
1645
1646 $lang = empty($this->userLanguage) ? acym_getLanguageTag() : $this->userLanguage;
1647 $setting = $this->config->get($type);
1648 $translation = $this->config->get('sender_info_translation');
1649
1650 if (!empty($translation)) {
1651 $translation = json_decode($translation, true);
1652
1653 if (!empty($translation[$lang][$type])) {
1654 $setting = $translation[$lang][$type];
1655 }
1656 }
1657
1658 return $setting;
1659 }
1660
1661 private function prepareEmailContent(int $mailId = 0): void
1662 {
1663 $this->handleRelativeURLs($mailId);
1664 $this->inlineCSS($mailId);
1665 $this->finalizeHtmlStructure($mailId);
1666 }
1667
1668 private function handleRelativeURLs(int $mailId): void
1669 {
1670 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1671 $mail->body = acym_absoluteURL($mail->body);
1672 }
1673
1674 private function inlineCSS(int $mailId): void
1675 {
1676 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1677 if (empty($mail->body)) {
1678 return;
1679 }
1680
1681 global $acymailingEmogrifiedMediaCSS;
1682 $acymailingEmogrifiedMediaCSS = '';
1683
1684 $style = $this->getEmailStylesheet($mail);
1685 // Inline styles for mail client compatibility: https://www.caniemail.com/features/html-style/
1686 // It also deletes the <style> tags from the body
1687 $cssInliner = CssInliner::fromHtml($mail->body)->inlineCss(implode('', $style));
1688 $domDocument = $cssInliner->getDomDocument();
1689
1690 // Remove the TinyMCE ids and the trailing ; in styles
1691 $mail->body = preg_replace(
1692 [
1693 '# id="mce_\d+"#Ui',
1694 '#(style="[^"]+);"#Ui',
1695 ],
1696 [
1697 '',
1698 '$1"',
1699 ],
1700 HtmlPruner::fromDomDocument($domDocument)
1701 ->removeRedundantClassesAfterCssInlined($cssInliner)
1702 ->renderBodyContent()
1703 );
1704
1705 // This character is added in the title and text blocks to prevent TinyMCE from adding an extra space when the block is empty
1706 $mail->body = str_replace('&zwj;', '', $mail->body);
1707
1708 // Remove the extra spaces in the style attributes
1709 $mail->body = preg_replace_callback(
1710 '#style="([^"]+)"#Ui',
1711 function ($matches) {
1712 return 'style="'.str_replace(['; ', ': '], [';', ':'], $matches[1]).'"';
1713 },
1714 $mail->body
1715 );
1716 }
1717
1718 private function finalizeHtmlStructure(int $mailId): void
1719 {
1720 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1721
1722 $finalContent = '<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">';
1723 $finalContent .= '<head>';
1724 $finalContent .= '<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->';
1725 $finalContent .= '<meta http-equiv="Content-Type" content="text/html; charset='.strtolower($this->config->get('charset')).'" />'."\n";
1726 $finalContent .= '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'."\n";
1727 $finalContent .= '<title>'.$mail->subject.'</title>'."\n";
1728
1729 //TODO maybe insert this CSS to a second <head> tag?
1730 // Source: https://designmodo.com/html-css-emails/#:~:text=words%20of%20wisdom-,Internal%C2%A0CSS,-Undoubtedly%2C%20one%20of
1731 // Apparently only for Yahoo on Android: https://www.caniemail.com/features/html-style/
1732 global $acymailingEmogrifiedMediaCSS;
1733 $finalContent .= '<style>'.$acymailingEmogrifiedMediaCSS.'</style>';
1734 $finalContent .= '<!--[if mso]><style type="text/css">#acym__wysid__template center > table { width: 580px; }</style><![endif]-->';
1735 $finalContent .= '<!--[if !mso]><!--><style>#acym__wysid__template center > table { width: 100%; }</style><!--<![endif]-->';
1736
1737 if (!empty($mail->headers)) {
1738 $finalContent .= $mail->headers;
1739 }
1740 $finalContent .= '</head>';
1741
1742 preg_match('@<[^>"t]*body[^>]*>@', $mail->body, $matches);
1743 if (empty($matches[0])) {
1744 $mail->body = '<body>'.$mail->body.'</body>';
1745 } else {
1746 preg_match('@<[^>"t]*/body[^>]*>@', $mail->body, $matches);
1747 if (empty($matches[0])) {
1748 $mail->body .= '</body>';
1749 }
1750 }
1751
1752 $finalContent .= $mail->body;
1753 $finalContent .= '</html>';
1754
1755 $mail->body = $finalContent;
1756 }
1757
1758 private function getEmailStylesheet(object $mail): array
1759 {
1760 $style = [];
1761
1762 // If this is a drag and drop mail we add foundation css for email
1763 if (strpos($mail->body, 'acym__wysid__template') !== false) {
1764 static $foundationCSS = null;
1765 if (empty($foundationCSS)) {
1766 $foundationCSS = acym_fileGetContent(ACYM_MEDIA.'css'.DS.'libraries'.DS.'foundation_email.min.css');
1767 // Remove the #acym__wysid__template prefix, not needed in sent emails
1768 $foundationCSS = str_replace('#acym__wysid__template ', '', $foundationCSS);
1769 }
1770 $style['foundation'] = $foundationCSS;
1771 }
1772
1773 static $emailFixes = null;
1774 if (empty($emailFixes)) {
1775 $emailFixes = acym_getEmailCssFixes();
1776 }
1777 $style['fixes'] = $emailFixes;
1778
1779 if (!empty($mail->stylesheet)) {
1780 $style['custom_stylesheet'] = $mail->stylesheet;
1781 }
1782
1783 $settingsStyles = $this->editorHelper->getSettingsStyle($mail->settings);
1784 if (!empty($settingsStyles)) {
1785 $style['settings_stylesheet'] = $settingsStyles;
1786 }
1787
1788 return $style;
1789 }
1790
1791 private function overrideAllEmails(array $options): bool
1792 {
1793 $this->trackEmail = false;
1794
1795 $this->addParam('subject', $options['subject']);
1796
1797 if (isset($options['isHtml']) && !$options['isHtml']) {
1798 $options['message'] = nl2br($options['message']);
1799 }
1800 $this->addParam('body', $options['message']);
1801
1802 $mailClass = new MailClass();
1803 $this->overrideEmailToSend = $mailClass->getOneByName('acy_notification_cms');
1804
1805 if (empty($this->overrideEmailToSend)) {
1806 return false;
1807 }
1808
1809 $statusSend = $this->sendOne($this->overrideEmailToSend->id, $options['to'], $options);
1810 if (!$statusSend && !empty($this->reportMessage)) {
1811 // Something went wrong when trying to send the override, log the information in the cron logs file
1812 $cronHelper = new CronHelper();
1813 $cronHelper->addMessage($this->reportMessage);
1814 $cronHelper->saveReport();
1815 }
1816
1817 return $statusSend;
1818 }
1819
1820 /* * * * * * * * * * * * * * * * *
1821 * *
1822 * Override PHPMailer's methods *
1823 * *
1824 * * * * * * * * * * * * * * * * */
1825
1826 /**
1827 * @throws Exception
1828 */
1829 private function acymAddReplyTo(string $email, string $name): void
1830 {
1831 if (empty($email)) {
1832 return;
1833 }
1834
1835 $replyToName = $this->config->get('add_names', true) ? $this->cleanText(trim($name)) : '';
1836 $replyToEmail = trim($email);
1837
1838 if (substr_count($replyToEmail, '@') > 1) {
1839 //We have more than one reply to...
1840 $replyToEmailArray = explode(';', str_replace([';', ','], ';', $replyToEmail));
1841 $replyToNameArray = explode(';', str_replace([';', ','], ';', $replyToName));
1842 foreach ($replyToEmailArray as $i => $oneReplyTo) {
1843 $this->addReplyTo($this->cleanText($oneReplyTo), @$replyToNameArray[$i]);
1844 }
1845 } else {
1846 $this->addReplyTo($this->cleanText($replyToEmail), $replyToName);
1847 }
1848 }
1849
1850 public function setFrom($email, $name = '', $auto = false)
1851 {
1852 if (!empty($email)) {
1853 $this->From = $this->cleanText($email);
1854 }
1855 if (!empty($name) && $this->config->get('add_names', true)) {
1856 $this->FromName = $this->cleanText($name);
1857 }
1858 }
1859
1860 /**
1861 * Outputs debugging info via user-defined method
1862 *
1863 * @param string $str
1864 */
1865 protected function edebug($str)
1866 {
1867 if (strpos($this->ErrorInfo, $str) === false) {
1868 $this->ErrorInfo .= ' '.$str;
1869 }
1870 }
1871
1872 public function getMailMIME()
1873 {
1874 $result = parent::getMailMIME();
1875
1876 //Added by Adrien on 11.02.2011 and then on 06 April 2011 otherwise we have 3 return char on phpMail or other functions
1877 $result = rtrim($result, static::$LE);
1878
1879 if ($this->Mailer != 'mail') {
1880 $result .= static::$LE.static::$LE;
1881 }
1882
1883 return $result;
1884 }
1885
1886 public static function validateAddress($address, $patternselect = null)
1887 {
1888 return true;
1889 }
1890 }
1891