PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / trunk
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress vtrunk
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 trunk, at back/Helpers/MailerHelper.php

1,894 lines 71.3 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 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 }
1894