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

1,863 lines 70.2 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 $filename = str_replace(['%', ' '], '_', basename($url));
1008 $filename = $this->removeAdditionalParams($filename);
1009
1010 $md5 = md5($filename);
1011 $cid = 'cid:'.$md5;
1012 $fileParts = explode(".", $filename);
1013 if (empty($fileParts[1])) {
1014 continue;
1015 }
1016 $ext = strtolower($fileParts[1]);
1017 // We only embed image files
1018 if (!isset($mimetypes[$ext])) {
1019 continue;
1020 }
1021
1022 // We only change the url if we were able to embed the image.
1023 if ($this->addEmbeddedImage($path, $md5, $filename, 'base64', $mimetypes[$ext])) {
1024 $this->Body = preg_replace('/'.preg_quote($images[0][$i], '/').'/Ui', $images[1][$i].'="'.$cid.'"', $this->Body);
1025 } else {
1026 $embedSuccess = false;
1027 }
1028 }
1029
1030 return $embedSuccess;
1031 }
1032
1033 public function cleanText(?string $text): string
1034 {
1035 return trim(preg_replace('/(%0A|%0D|\n+|\r+)/i', '', (string)$text));
1036 }
1037
1038 /**
1039 * Adds shortcodes available in the email content
1040 */
1041 public function addParam(string $name, $value): void
1042 {
1043 $tagName = '{'.$name.'}';
1044 $this->parameters[$tagName] = $value;
1045 }
1046
1047 public function overrideEmail(array $options): bool
1048 {
1049 $overrideAllEmails = $this->config->get('send_website_emails', 0) == 1;
1050
1051 $overrideClass = new OverrideClass();
1052 $override = $overrideClass->getMailByBaseContent($options['subject'], $options['message']);
1053
1054 // We let the CMS send the email
1055 if (empty($override) && !$overrideAllEmails) {
1056 return false;
1057 }
1058
1059 // AcyMailing needs to send the email
1060 $this->autoAddUser = true;
1061
1062 if (empty($override)) {
1063 $this->trackEmail = false;
1064
1065 return $this->overrideAllEmails($options);
1066 } else {
1067 $this->trackEmail = true;
1068
1069 return $this->sendOverride($override, $options);
1070 }
1071 }
1072
1073 public function isPortOpen(int $port, string $targetServer): bool
1074 {
1075 if (!function_exists('fsockopen')) {
1076 $this->portError = acym_translation('ACYM_FSOCKOPEN');
1077
1078 return false;
1079 }
1080
1081 $errorCode = 0;
1082 $errorMessage = 0;
1083 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fsockopen -- Connectivity test for SMTP ports.
1084 $fp = @fsockopen($targetServer, $port, $errorCode, $errorMessage, 5);
1085
1086 if (is_resource($fp)) {
1087 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing the socket resource opened above, not a filesystem handle.
1088 fclose($fp);
1089
1090 return true;
1091 } else {
1092 $this->portError = $errorCode.' - '.acym_utf8Encode($errorMessage);
1093
1094 return false;
1095 }
1096 }
1097
1098 private function getSendingMethodSettings(string $settingName, string $default = ''): string
1099 {
1100 if (empty($this->currentMethodSetting)) {
1101 return $this->config->get($settingName, $default);
1102 }
1103
1104 return $this->currentMethodSetting[$settingName] ?? $default;
1105 }
1106
1107 private function getSendingMethod(): string
1108 {
1109 $this->currentMethodSetting = [];
1110
1111 $sendingMethods = $this->config->get('sending_method_list', '{}');
1112 $sendingMethods = json_decode($sendingMethods, true);
1113 $sendingMethodsByList = $this->config->get('sending_method_list_by_list', '{}');
1114 $sendingMethodsByList = json_decode($sendingMethodsByList, true);
1115
1116 if (empty($this->listsIds) || empty($sendingMethods) || empty($sendingMethodsByList) || empty($sendingMethodsByList[$this->listsIds[0]])) {
1117 return $this->config->get('mailer_method', 'phpmail');
1118 }
1119
1120 $sendingMethodIds = [];
1121
1122 foreach ($this->listsIds as $listId) {
1123 if (!empty($sendingMethodsByList[$listId])) {
1124 $sendingMethodIds[] = $sendingMethodsByList[$listId];
1125 }
1126 }
1127
1128 if (count($sendingMethodIds) !== count($this->listsIds) || count(array_unique($sendingMethodIds)) !== 1) {
1129 return $this->config->get('mailer_method', 'phpmail');
1130 }
1131
1132 $sendingMethodId = $sendingMethodIds[0];
1133
1134 if (empty($sendingMethods[$sendingMethodId])) {
1135 return $this->config->get('mailer_method', 'phpmail');
1136 }
1137
1138 $this->currentMethodSetting = $sendingMethods[$sendingMethodId];
1139
1140 return $sendingMethods[$sendingMethodId]['mailer_method'];
1141 }
1142
1143 private function isExternal(string $method): void
1144 {
1145 $this->Mailer = 'external';
1146 $this->externalMailer = $method;
1147 }
1148
1149 private function storeArchiveVersion(int $mailId): void
1150 {
1151 if ($this->defaultMail[$mailId]->type !== MailClass::TYPE_STANDARD) {
1152 return;
1153 }
1154
1155 $mailArchive = new \stdClass();
1156
1157 $alreadyExisting = $this->mailArchiveClass->getOneByMailId($mailId);
1158 if (!empty($alreadyExisting)) {
1159 $mailArchive->id = $alreadyExisting->id;
1160 }
1161
1162 $mailArchive->mail_id = $mailId;
1163 $mailArchive->date = time();
1164 $mailArchive->body = $this->defaultMail[$mailId]->body;
1165 $mailArchive->subject = $this->defaultMail[$mailId]->subject;
1166 $mailArchive->settings = $this->defaultMail[$mailId]->settings;
1167 $mailArchive->stylesheet = $this->defaultMail[$mailId]->stylesheet;
1168 $mailArchive->attachments = $this->defaultMail[$mailId]->attachments;
1169 $this->mailArchiveClass->save($mailArchive);
1170 }
1171
1172 private function canTrack(int $mailId, object $user): bool
1173 {
1174 if ($this->isTest || empty($mailId) || empty($user) || !isset($user->tracking) || $user->tracking != 1) {
1175 return false;
1176 }
1177
1178 $mail = $this->mailClass->getOneById($mailId);
1179 if (!empty($mail) && $mail->tracking != 1) {
1180 return false;
1181 }
1182
1183 $lists = $this->mailClass->getAllListsByMailIdAndUserId($mailId, $user->id);
1184
1185 foreach ($lists as $list) {
1186 if ($list->tracking != 1) {
1187 return false;
1188 }
1189 }
1190
1191 return true;
1192 }
1193
1194 /**
1195 * @param mixed $user
1196 *
1197 * @throws Exception
1198 */
1199 private function loadUser($user, bool $force = false): object
1200 {
1201 if (is_numeric($user)) {
1202 $receiver = $this->userClass->getOneById($user);
1203 } elseif (is_object($user)) {
1204 $receiver = $user;
1205 } elseif (is_string($user) && strpos($user, '@')) {
1206 acym_setVar('acy_source', 'When sending a test');
1207 $this->userClass->triggers = $this->userCreationTriggers;
1208 $receiver = $this->userClass->getOneByEmail($user, $force || $this->autoAddUser);
1209 }
1210
1211 if (empty($receiver)) {
1212 throw new Exception(esc_attr(acym_translation('ACYM_USER_NOT_FOUND')));
1213 }
1214
1215 $this->userLanguage = empty($receiver->language) ? acym_getLanguageTag() : $receiver->language;
1216 $this->receiverEmail = (string)($receiver->email ?? '');
1217
1218 return $receiver;
1219 }
1220
1221 private function prepareContent(int $mailId, array $options): void
1222 {
1223 $this->isHTML();
1224
1225 if (!empty($this->defaultMail[$mailId]->stylesheet)) {
1226 $this->stylesheet = $this->defaultMail[$mailId]->stylesheet;
1227 }
1228 $this->settings = empty($this->defaultMail[$mailId]->settings) ? [] : json_decode($this->defaultMail[$mailId]->settings, true);
1229
1230 $this->Subject = $this->defaultMail[$mailId]->subject;
1231 $this->Body = $this->defaultMail[$mailId]->body;
1232 if ($this->isTest && !empty($options['testNote'])) {
1233 $this->Body = '<div style="text-align: center; padding: 25px; font-family: Poppins; font-size: 20px">'.$options['testNote'].'</div>'.$this->Body;
1234 }
1235
1236 //We add the intro text at the top of the body
1237 if (!empty($this->introtext)) {
1238 $this->Body = $this->introtext.$this->Body;
1239 }
1240
1241 if (!empty($this->defaultMail[$mailId]->preheader)) {
1242 $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;">';
1243 $preHeader .= $this->defaultMail[$mailId]->preheader;
1244 $preHeader .= '</div><!--<![endif]-->';
1245
1246 //We want to insert the preview at the start of the body so we match the start of the mail
1247 preg_match('#(<(.*)<body(.*)>)#Uis', $this->Body, $matches);
1248 if (empty($matches) || empty($matches[1])) {
1249 $this->Body = $preHeader.$this->Body;
1250 } else {
1251 $this->Body = $matches[1].$preHeader.str_replace($matches[1], '', $this->Body);
1252 }
1253 }
1254
1255 if (ACYM_CMS === 'wordpress') {
1256 ob_start();
1257 $this->Body = do_shortcode($this->Body);
1258 ob_end_clean();
1259 }
1260 }
1261
1262 private function prepareHeaders(int $mailId, int $receiverId, array $options): void
1263 {
1264 // Specify a messageID which will be kept by most mail clients and in the feedback loop
1265 $subject = base64_encode(acym_rand(0, 9999999)).'AC'.$receiverId.'Y'.$this->defaultMail[$mailId]->id.'BA'.base64_encode(time().acym_rand(0, 99999));
1266 $this->MessageID = '<'.preg_replace('|[^a-z0-9+_]|i', '', $subject).'@'.$this->serverHostname().'>';
1267 $this->addCustomHeader('Feedback-ID', $this->defaultMail[$mailId]->id.':'.$receiverId.':'.$this->defaultMail[$mailId]->type.':'.base64_encode(ACYM_ROOT));
1268
1269 if (!empty($this->defaultMail[$mailId]->headers)) {
1270 $this->mailHeader = $this->defaultMail[$mailId]->headers;
1271 }
1272
1273 if (!empty($options['headers'])) {
1274 foreach ($options['headers'] as $headerName => $headerValue) {
1275 $this->addCustomHeader(trim($headerName), trim($headerValue));
1276 }
1277 }
1278 }
1279
1280 private function prepareAddresses(int $mailId, object $receiver): void
1281 {
1282 $receiverName = '';
1283 if ($this->config->get('add_names', true)) {
1284 $receiverName = $this->cleanText($receiver->name);
1285 //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
1286 if ($receiverName == $this->cleanText($receiver->email)) {
1287 $receiverName = '';
1288 }
1289 }
1290 $this->addAddress($this->cleanText($receiver->email), $receiverName);
1291
1292 $this->setFrom($this->getSendSettings('from_email', $mailId), $this->getSendSettings('from_name', $mailId));
1293 $this->acymAddReplyTo($this->defaultMail[$mailId]->reply_to_email ?? '', $this->defaultMail[$mailId]->reply_to_name ?? '');
1294
1295 // This is the bounce email address
1296 if (!empty($this->defaultMail[$mailId]->bounce_email)) {
1297 $this->Sender = $this->cleanText($this->defaultMail[$mailId]->bounce_email);
1298 } else {
1299 $this->Sender = $this->cleanText($this->config->get('bounce_email'));
1300 }
1301 }
1302
1303 /**
1304 * @throws Exception
1305 */
1306 private function prepareBcc(int $mailId, array $options): void
1307 {
1308 $bccAddresses = [];
1309
1310 if (!empty($options['bcc'])) {
1311 $bccAddresses = $options['bcc'];
1312 }
1313
1314 if (!empty($this->defaultMail[$mailId]->bcc)) {
1315 $emailBccAddresses = explode(';', trim(str_replace([',', ' '], ';', $this->defaultMail[$mailId]->bcc)));
1316 $bccAddresses = array_unique(array_merge($bccAddresses, $emailBccAddresses));
1317 }
1318
1319 if (!empty($bccAddresses)) {
1320 $bccAddresses = array_filter($bccAddresses);
1321
1322 // loadUser() overwrites the current receiver, keep the real one
1323 $receiverEmail = $this->receiverEmail;
1324 $userLanguage = $this->userLanguage;
1325
1326 foreach ($bccAddresses as $oneBccAddress) {
1327 $this->loadUser($oneBccAddress, true);
1328 $this->addBCC($oneBccAddress);
1329 }
1330
1331 $this->receiverEmail = $receiverEmail;
1332 $this->userLanguage = $userLanguage;
1333 }
1334 }
1335
1336 private function prepareAttachments(int $mailId, array $options): void
1337 {
1338 $attachments = [];
1339
1340 if (!empty($this->defaultMail[$mailId]->attach)) {
1341 $attachments = $this->defaultMail[$mailId]->attach;
1342 }
1343
1344 if (!empty($options['attachments'])) {
1345 $attachments = array_merge($attachments, $options['attachments']);
1346 }
1347
1348 if (empty($attachments)) {
1349 return;
1350 }
1351
1352 if ($this->config->get('embed_files')) {
1353 foreach ($attachments as $attachment) {
1354 $this->addAttachment($attachment->filename, $attachment->name);
1355 }
1356 } else {
1357 $attachStringHTML = '<fieldset><legend>'.acym_translation('ACYM_ATTACHMENTS').'</legend><table>';
1358 foreach ($attachments as $attachment) {
1359 $attachStringHTML .= '<tr><td><a href="'.$attachment->url.'" target="_blank">'.$attachment->name.'</a></td></tr>';
1360 }
1361 $attachStringHTML .= '</table></fieldset>';
1362
1363 if ($this->config->get('attachments_position', 'bottom') === 'top') {
1364 $this->Body = $attachStringHTML.'<br />'.$this->Body;
1365 } else {
1366 $this->Body .= '<br />'.$attachStringHTML;
1367 }
1368 }
1369 }
1370
1371 private function replaceDynamicContent(int $mailId): void
1372 {
1373 $this->replaceParams();
1374
1375 // Left side are mail columns on which we replace the short codes
1376 $this->body = &$this->Body;
1377 $this->altbody = &$this->AltBody;
1378 $this->subject = &$this->Subject;
1379 $this->from = &$this->From;
1380 $this->fromName = &$this->FromName;
1381 $this->replyto = &$this->ReplyTo;
1382 $this->replyname = $this->defaultMail[$mailId]->reply_to_name ?? '';
1383 $this->replyemail = $this->defaultMail[$mailId]->reply_to_email ?? '';
1384 $this->mailId = intval($this->defaultMail[$mailId]->id);
1385 $this->id = $this->mailId;
1386 $this->creator_id = intval($this->defaultMail[$mailId]->creator_id);
1387 $this->type = $this->defaultMail[$mailId]->type;
1388 $this->stylesheet = &$this->stylesheet;
1389 $this->links_language = $this->defaultMail[$mailId]->links_language;
1390 }
1391
1392 private function stripAutologinParams(string $url): string
1393 {
1394 return str_replace(
1395 ['?'.self::AUTOLOGIN_PARAMS.'&amp;', '?'.self::AUTOLOGIN_PARAMS, '&amp;'.self::AUTOLOGIN_PARAMS],
1396 ['?', '', ''],
1397 $url
1398 );
1399 }
1400
1401 private function prepareTracking(int $mailId, object $receiver): void
1402 {
1403 if ($this->canTrack($mailId, $receiver)) {
1404 $receiver->id = intval($receiver->id);
1405 $this->statPicture($this->mailId, $receiver->id);
1406 $this->body = acym_absoluteURL($this->body);
1407 $this->statClick($this->mailId, $receiver->id, false, $receiver->key ?? '');
1408 if (acym_isTrackingSalesActive()) {
1409 $this->trackingSales($this->mailId, $receiver->id);
1410 }
1411 }
1412 }
1413
1414 private function handleDtexts(object $receiver): bool
1415 {
1416 $this->replaceParams();
1417
1418 // Sending a spam-test, use the current user instead
1419 if (strpos($receiver->email, '@mt.acyba.com') !== false) {
1420 $currentUser = $this->userClass->getOneByEmail(acym_currentUserEmail());
1421 if (empty($currentUser)) {
1422 $currentUser = $receiver;
1423 }
1424 $result = acym_trigger('replaceUserInformation', [&$this, &$currentUser, true]);
1425 } else {
1426 $result = acym_trigger('replaceUserInformation', [&$this, &$receiver, true]);
1427 if (!empty($result)) {
1428 foreach ($result as $oneResult) {
1429 if (!empty($oneResult) && !$oneResult['send']) {
1430 $this->reportMessage = $oneResult['message'];
1431
1432 return false;
1433 }
1434 }
1435 }
1436 }
1437
1438 global $acymLanguages;
1439 if (!empty($acymLanguages['userLanguage'])) {
1440 unset($acymLanguages['userLanguage']);
1441 }
1442
1443 // A user based dtext may have inserted HTML content, we need to re-inline the CSS to it
1444 foreach ($result as $oneResult) {
1445 if (!empty($oneResult['emogrifier'])) {
1446 $this->prepareEmailContent();
1447 break;
1448 }
1449 }
1450
1451 if ($this->config->get('multiple_part', false)) {
1452 $bodyForText = preg_replace('#<head\b[^>]*>.*?</head\s*>#is', '', $this->Body);
1453 $this->altbody = $this->textVersion($bodyForText);
1454 }
1455
1456 $this->replaceParams();
1457
1458 return true;
1459 }
1460
1461 private function initStatistics(bool $status, int $receiverId): void
1462 {
1463 if ($this->trackEmail) {
1464 $statsAdd = [];
1465 $statsAdd[$this->mailId][intval($status)][] = $receiverId;
1466
1467 $queueHelper = new QueueHelper();
1468 $queueHelper->statsAdd($statsAdd);
1469 $this->trackEmail = false;
1470 }
1471 }
1472
1473 private function trackingSales(int $mailId, int $userId): void
1474 {
1475 preg_match_all('#href[ ]*=[ ]*"(?!mailto:|\#|ymsgr:|callto:|file:|ftp:|webcal:|skype:|tel:)([^"]+)"#Ui', $this->body, $results);
1476 if (empty($results)) {
1477 return;
1478 }
1479
1480 foreach ($results[1] as $url) {
1481 $simplifiedUrl = str_replace(['https://', 'http://', 'www.'], '', $url);
1482 $simplifiedWebsite = str_replace(['https://', 'http://', 'www.'], '', ACYM_LIVE);
1483 if (strpos($simplifiedUrl, rtrim($simplifiedWebsite, '/')) === false || strpos($url, 'task=unsub')) continue;
1484
1485 $toAddUrl = (strpos($url, '?') === false ? '?' : '&').'linkReferal='.$mailId.'-'.$userId;
1486
1487 $posHash = strpos($url, '#');
1488 if ($posHash !== false) {
1489 $newURL = substr($url, 0, $posHash).$toAddUrl.substr($url, $posHash);
1490 } else {
1491 $newURL = $url.$toAddUrl;
1492 }
1493
1494 $this->body = preg_replace('#href="('.preg_quote($url, '#').')"#Uis', 'href="'.$newURL.'"', $this->body);
1495 }
1496 }
1497
1498 private function statPicture(int $mailId, int $userId): void
1499 {
1500 $pictureLink = acym_frontendLink('frontstats&task=openStats&id='.$mailId.'&userid='.$userId, true, false);
1501
1502 $statPicture = '<img class="spict" alt="" src="'.$pictureLink.'" border="0" width="50" height="1" />';
1503
1504 if (strpos($this->body, '</body>')) {
1505 $this->body = str_replace('</body>', $statPicture.'</body>', $this->body);
1506 } else {
1507 $this->body .= $statPicture;
1508 }
1509 }
1510
1511 private function removeAdditionalParams(string $url): string
1512 {
1513 $additionalParamsPos = strpos($url, '?');
1514 if (!empty($additionalParamsPos)) {
1515 $url = substr($url, 0, $additionalParamsPos);
1516 }
1517
1518 return $url;
1519 }
1520
1521 private function replaceParams(): void
1522 {
1523 if (empty($this->parameters)) {
1524 return;
1525 }
1526
1527 $pluginHelper = new PluginHelper();
1528
1529 //We create an extra tag which contains all possible parameters...
1530 $this->generateAllParams();
1531
1532 $vars = [
1533 'Subject',
1534 'Body',
1535 'From',
1536 'FromName',
1537 'replyname',
1538 'replyemail',
1539 ];
1540
1541 foreach ($vars as $oneVar) {
1542 if (!empty($this->$oneVar)) {
1543 $this->$oneVar = $pluginHelper->replaceDText($this->$oneVar, $this->parameters);
1544 }
1545 }
1546
1547 if (!empty($this->ReplyTo)) {
1548 foreach ($this->ReplyTo as $i => $replyto) {
1549 foreach ($replyto as $a => $oneval) {
1550 $this->ReplyTo[$i][$a] = $pluginHelper->replaceDText($this->ReplyTo[$i][$a], $this->parameters);
1551 }
1552 }
1553 }
1554 }
1555
1556 /**
1557 * Create a new parameter called {alltags} which will include all the others
1558 */
1559 private function generateAllParams(): void
1560 {
1561 $result = '<table style="border:1px solid;border-collapse:collapse;" border="1" cellpadding="10"><tr><td>Tag</td><td>Value</td></tr>';
1562 foreach ($this->parameters as $name => $value) {
1563 //Just in case of...
1564 if (!is_string($value)) continue;
1565
1566 $result .= '<tr><td>'.trim($name, '{}').'</td><td>'.$value.'</td></tr>';
1567 }
1568 $result .= '</table>';
1569 $this->addParam('allshortcodes', $result);
1570 }
1571
1572 private function sendOverride(object $override, array $options): bool
1573 {
1574 // 2 - Prepare the email and params
1575 for ($i = 1; $i < count($override->parameters); $i++) {
1576 $oneParam = $override->parameters[$i];
1577
1578 // Joomla emails have links as text, convert them
1579 $unmodified = $oneParam;
1580 $oneParam = preg_replace(
1581 '/(http|https):\/\/(.*)/',
1582 '<a href="$1://$2" target="_blank">$1://$2</a>',
1583 $oneParam,
1584 -1,
1585 $count
1586 );
1587 if ($count > 0) {
1588 $this->addParam('link'.$i, $unmodified);
1589 }
1590 $this->addParam('param'.$i, $oneParam);
1591 }
1592
1593 $this->addParam('subject', $options['subject']);
1594
1595 // 3 - Send the email
1596 $this->overrideEmailToSend = $override;
1597 $statusSend = $this->sendOne($override->id, $options['to'], $options);
1598 if (!$statusSend && !empty($this->reportMessage)) {
1599 // Something went wrong when trying to send the override, log the information in the cron logs file
1600 $cronHelper = new CronHelper();
1601 $cronHelper->saveReport([$this->reportMessage]);
1602 }
1603
1604 return $statusSend;
1605 }
1606
1607 public function getSendSettings(string $type, int $mailId = 0): string
1608 {
1609 if (!in_array($type, ['from_name', 'from_email', 'replyto_name', 'replyto_email', 'bounce_email'])) {
1610 return '';
1611 }
1612
1613 $mailType = strpos($type, 'replyto') !== false ? str_replace('replyto', 'reply_to', $type) : $type;
1614 if (!empty($mailId) && !empty($this->defaultMail[$mailId]->$mailType)) {
1615 return $this->defaultMail[$mailId]->$mailType;
1616 }
1617
1618 $lang = empty($this->userLanguage) ? acym_getLanguageTag() : $this->userLanguage;
1619 $setting = $this->config->get($type);
1620 $translation = $this->config->get('sender_info_translation');
1621
1622 if (!empty($translation)) {
1623 $translation = json_decode($translation, true);
1624
1625 if (!empty($translation[$lang][$type])) {
1626 $setting = $translation[$lang][$type];
1627 }
1628 }
1629
1630 return $setting;
1631 }
1632
1633 private function prepareEmailContent(int $mailId = 0): void
1634 {
1635 $this->handleRelativeURLs($mailId);
1636 $this->inlineCSS($mailId);
1637 $this->finalizeHtmlStructure($mailId);
1638 }
1639
1640 private function handleRelativeURLs(int $mailId): void
1641 {
1642 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1643 $mail->body = acym_absoluteURL($mail->body);
1644 }
1645
1646 private function inlineCSS(int $mailId): void
1647 {
1648 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1649 if (empty($mail->body)) {
1650 return;
1651 }
1652
1653 global $acymailingEmogrifiedMediaCSS;
1654 $acymailingEmogrifiedMediaCSS = '';
1655
1656 $style = $this->getEmailStylesheet($mail);
1657 // Inline styles for mail client compatibility: https://www.caniemail.com/features/html-style/
1658 // It also deletes the <style> tags from the body
1659 $cssInliner = CssInliner::fromHtml($mail->body)->inlineCss(implode('', $style));
1660 $domDocument = $cssInliner->getDomDocument();
1661
1662 // Remove the TinyMCE ids and the trailing ; in styles
1663 $mail->body = preg_replace(
1664 [
1665 '# id="mce_\d+"#Ui',
1666 '#(style="[^"]+);"#Ui',
1667 ],
1668 [
1669 '',
1670 '$1"',
1671 ],
1672 HtmlPruner::fromDomDocument($domDocument)
1673 ->removeRedundantClassesAfterCssInlined($cssInliner)
1674 ->renderBodyContent()
1675 );
1676
1677 // This character is added in the title and text blocks to prevent TinyMCE from adding an extra space when the block is empty
1678 $mail->body = str_replace('&zwj;', '', $mail->body);
1679
1680 // Remove the extra spaces in the style attributes
1681 $mail->body = preg_replace_callback(
1682 '#style="([^"]+)"#Ui',
1683 function ($matches) {
1684 return 'style="'.str_replace(['; ', ': '], [';', ':'], $matches[1]).'"';
1685 },
1686 $mail->body
1687 );
1688 }
1689
1690 private function finalizeHtmlStructure(int $mailId): void
1691 {
1692 $mail = empty($mailId) ? $this : $this->defaultMail[$mailId];
1693
1694 $finalContent = '<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">';
1695 $finalContent .= '<head>';
1696 $finalContent .= '<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->';
1697 $finalContent .= '<meta http-equiv="Content-Type" content="text/html; charset='.strtolower($this->config->get('charset')).'" />'."\n";
1698 $finalContent .= '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'."\n";
1699 $finalContent .= '<title>'.$mail->subject.'</title>'."\n";
1700
1701 //TODO maybe insert this CSS to a second <head> tag?
1702 // Source: https://designmodo.com/html-css-emails/#:~:text=words%20of%20wisdom-,Internal%C2%A0CSS,-Undoubtedly%2C%20one%20of
1703 // Apparently only for Yahoo on Android: https://www.caniemail.com/features/html-style/
1704 global $acymailingEmogrifiedMediaCSS;
1705 $finalContent .= '<style>'.$acymailingEmogrifiedMediaCSS.'</style>';
1706 $finalContent .= '<!--[if mso]><style type="text/css">#acym__wysid__template center > table { width: 580px; }</style><![endif]-->';
1707 $finalContent .= '<!--[if !mso]><!--><style>#acym__wysid__template center > table { width: 100%; }</style><!--<![endif]-->';
1708
1709 if (!empty($mail->headers)) {
1710 $finalContent .= $mail->headers;
1711 }
1712 $finalContent .= '</head>';
1713
1714 preg_match('@<[^>"t]*body[^>]*>@', $mail->body, $matches);
1715 if (empty($matches[0])) {
1716 $mail->body = '<body>'.$mail->body.'</body>';
1717 } else {
1718 preg_match('@<[^>"t]*/body[^>]*>@', $mail->body, $matches);
1719 if (empty($matches[0])) {
1720 $mail->body .= '</body>';
1721 }
1722 }
1723
1724 $finalContent .= $mail->body;
1725 $finalContent .= '</html>';
1726
1727 $mail->body = $finalContent;
1728 }
1729
1730 private function getEmailStylesheet(object $mail): array
1731 {
1732 $style = [];
1733
1734 // If this is a drag and drop mail we add foundation css for email
1735 if (strpos($mail->body, 'acym__wysid__template') !== false) {
1736 static $foundationCSS = null;
1737 if (empty($foundationCSS)) {
1738 $foundationCSS = acym_fileGetContent(ACYM_MEDIA.'css'.DS.'libraries'.DS.'foundation_email.min.css');
1739 // Remove the #acym__wysid__template prefix, not needed in sent emails
1740 $foundationCSS = str_replace('#acym__wysid__template ', '', $foundationCSS);
1741 }
1742 $style['foundation'] = $foundationCSS;
1743 }
1744
1745 static $emailFixes = null;
1746 if (empty($emailFixes)) {
1747 $emailFixes = acym_getEmailCssFixes();
1748 }
1749 $style['fixes'] = $emailFixes;
1750
1751 if (!empty($mail->stylesheet)) {
1752 $style['custom_stylesheet'] = $mail->stylesheet;
1753 }
1754
1755 $settingsStyles = $this->editorHelper->getSettingsStyle($mail->settings);
1756 if (!empty($settingsStyles)) {
1757 $style['settings_stylesheet'] = $settingsStyles;
1758 }
1759
1760 return $style;
1761 }
1762
1763 private function overrideAllEmails(array $options): bool
1764 {
1765 $this->trackEmail = false;
1766
1767 $this->addParam('subject', $options['subject']);
1768
1769 if (isset($options['isHtml']) && !$options['isHtml']) {
1770 $options['message'] = nl2br($options['message']);
1771 }
1772 $this->addParam('body', $options['message']);
1773
1774 $mailClass = new MailClass();
1775 $this->overrideEmailToSend = $mailClass->getOneByName('acy_notification_cms');
1776
1777 if (empty($this->overrideEmailToSend)) {
1778 return false;
1779 }
1780
1781 $statusSend = $this->sendOne($this->overrideEmailToSend->id, $options['to'], $options);
1782 if (!$statusSend && !empty($this->reportMessage)) {
1783 // Something went wrong when trying to send the override, log the information in the cron logs file
1784 $cronHelper = new CronHelper();
1785 $cronHelper->addMessage($this->reportMessage);
1786 $cronHelper->saveReport();
1787 }
1788
1789 return $statusSend;
1790 }
1791
1792 /* * * * * * * * * * * * * * * * *
1793 * *
1794 * Override PHPMailer's methods *
1795 * *
1796 * * * * * * * * * * * * * * * * */
1797
1798 /**
1799 * @throws Exception
1800 */
1801 private function acymAddReplyTo(string $email, string $name): void
1802 {
1803 if (empty($email)) {
1804 return;
1805 }
1806
1807 $replyToName = $this->config->get('add_names', true) ? $this->cleanText(trim($name)) : '';
1808 $replyToEmail = trim($email);
1809
1810 if (substr_count($replyToEmail, '@') > 1) {
1811 //We have more than one reply to...
1812 $replyToEmailArray = explode(';', str_replace([';', ','], ';', $replyToEmail));
1813 $replyToNameArray = explode(';', str_replace([';', ','], ';', $replyToName));
1814 foreach ($replyToEmailArray as $i => $oneReplyTo) {
1815 $this->addReplyTo($this->cleanText($oneReplyTo), @$replyToNameArray[$i]);
1816 }
1817 } else {
1818 $this->addReplyTo($this->cleanText($replyToEmail), $replyToName);
1819 }
1820 }
1821
1822 public function setFrom($email, $name = '', $auto = false)
1823 {
1824 if (!empty($email)) {
1825 $this->From = $this->cleanText($email);
1826 }
1827 if (!empty($name) && $this->config->get('add_names', true)) {
1828 $this->FromName = $this->cleanText($name);
1829 }
1830 }
1831
1832 /**
1833 * Outputs debugging info via user-defined method
1834 *
1835 * @param string $str
1836 */
1837 protected function edebug($str)
1838 {
1839 if (strpos($this->ErrorInfo, $str) === false) {
1840 $this->ErrorInfo .= ' '.$str;
1841 }
1842 }
1843
1844 public function getMailMIME()
1845 {
1846 $result = parent::getMailMIME();
1847
1848 //Added by Adrien on 11.02.2011 and then on 06 April 2011 otherwise we have 3 return char on phpMail or other functions
1849 $result = rtrim($result, static::$LE);
1850
1851 if ($this->Mailer != 'mail') {
1852 $result .= static::$LE.static::$LE;
1853 }
1854
1855 return $result;
1856 }
1857
1858 public static function validateAddress($address, $patternselect = null)
1859 {
1860 return true;
1861 }
1862 }
1863