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 / BounceHelper.php

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

1,583 lines 64.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace AcyMailing\Helpers;
4
5 use AcyMailerPhp\OAuth;
6 use AcyMailing\Classes\HistoryClass;
7 use AcyMailing\Classes\ListClass;
8 use AcyMailing\Classes\MailClass;
9 use AcyMailing\Classes\RuleClass;
10 use AcyMailing\Classes\UserClass;
11 use AcyMailing\Core\AcymObject;
12 use AcyMailing\Libraries\Pear\Pop3;
13 use AcyMailing\Types\CharsetType;
14
15 class BounceHelper extends AcymObject
16 {
17 const HOSTS_NEEDING_OAUTH = [
18 'imap.gmail.com',
19 'outlook.office365.com',
20 ];
21
22 const MESSAGE_TYPE_SUCCESS = 0;
23 const MESSAGE_TYPE_INFO = 1;
24 const MESSAGE_TYPE_ERROR = 2;
25
26 // Needed information for the connection
27 private string $server;
28 private string $username;
29 private string $password;
30 private string $port;
31 private string $connectMethod;
32 private string $secureMethod;
33 private bool $selfSigned;
34 private int $timeout;
35 private string $oAuthToken;
36
37 // Allowed extensions for uploaded files (attachments)
38 private array $allowed_extensions = [];
39 protected int $nbMessages = 0;
40 public bool $report = false;
41 private MailerHelper $mailer;
42 private $mailbox;
43 public $_message;
44 private UserClass $userClass;
45 private array $blockedUsers = [];
46 private array $deletedUsers = [];
47 private array $bounceMessages = [];
48 private bool $usePear = false;
49 private string $detectEmail;
50 private string $detectEmail2 = '/(([a-z0-9\-]+\.)+[a-z0-9]{2,8})\/([a-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+)*)/i';
51 public array $messages = [];
52 //Max execution time minus 4 seconds... we need to stop the process before this date.
53 public int $stoptime = 0;
54 //Check apache module "mod_security" to avoid flush issue
55 protected bool $mod_security2 = false;
56 //Number of ob_end_flush used in the process...
57 public int $obend = 0;
58 // Correspond to mailbox entity in database
59 public object $action;
60 public array $attachments = [];
61
62 private array $inlineImages = [];
63 private array $allCharsets;
64 private bool $errorDuringActions = false;
65
66 private RuleClass $ruleClass;
67 private MailClass $mailClass;
68 private HistoryClass $historyClass;
69 private EncodingHelper $encodingHelper;
70
71 private array $allLists;
72
73 public function __construct()
74 {
75 parent::__construct();
76
77 $this->mailer = new MailerHelper();
78 $this->ruleClass = new RuleClass();
79 $this->userClass = new UserClass();
80 $this->mailClass = new MailClass();
81 $this->historyClass = new HistoryClass();
82 $this->encodingHelper = new EncodingHelper();
83
84 $this->mailer->report = false;
85 $this->mailer->alreadyCheckedAddresses = true;
86
87 $charsetType = new CharsetType();
88 $this->allCharsets = $charsetType->charsets;
89
90 $this->allowed_extensions = explode(',', $this->config->get('allowed_files', ''));
91 $this->detectEmail = '/'.acym_getEmailRegex(false, true).'/i';
92 }
93
94 public function init(array $config = []): bool
95 {
96 if (empty($config)) {
97 $config = [
98 'server' => $this->config->get('bounce_server'),
99 'username' => $this->config->get('bounce_username'),
100 'password' => $this->config->get('bounce_password'),
101 'port' => $this->config->get('bounce_port'),
102 'connect_method' => $this->config->get('bounce_connection'),
103 'secure_method' => $this->config->get('bounce_secured'),
104 'self_signed' => $this->config->get('bounce_certif', false),
105 'timeout' => $this->config->get('bounce_timeout', 10),
106 'bounce_access_token' => $this->config->get('bounce_access_token'),
107 ];
108 }
109
110 $this->server = $config['server'];
111 $this->username = $config['username'];
112 $this->password = $config['password'];
113 $this->port = $config['port'];
114 $this->connectMethod = $config['connect_method'];
115 $this->secureMethod = $config['secure_method'];
116 $this->selfSigned = $config['self_signed'];
117 $this->timeout = intval($config['timeout']);
118 $this->oAuthToken = $config['bounce_access_token'];
119
120 if ($this->connectMethod === 'pear') {
121 $this->usePear = true;
122 include_once ACYM_LIBRARIES.'Pear'.DS.'Pop3.php';
123
124 return true;
125 }
126
127 require_once ACYM_LIBRARIES.'Imap2'.DS.'bootstrap.php';
128
129 if (extension_loaded('imap') || function_exists('imap_open')) {
130 return true;
131 }
132
133 if ($this->report) {
134 $prefix = PHP_SHLIB_SUFFIX === 'dll' ? 'php_' : '';
135 $extension = $prefix.'imap.'.PHP_SHLIB_SUFFIX;
136
137 acym_display(
138 'The extension "'.$extension.'" could not be loaded, please change your PHP configuration to enable it or use the pop3 method without imap extension',
139 'error'
140 );
141 }
142
143 return false;
144 }
145
146 public function connect(): bool
147 {
148 if ($this->usePear) {
149 return $this->connectPear();
150 }
151
152 return $this->connectImap();
153 }
154
155 private function connectPear(): bool
156 {
157 ob_start();
158
159 $this->mailbox = new Pop3();
160
161 $timeout = $this->timeout;
162 if (!empty($timeout)) {
163 $this->mailbox->_timeout = $timeout;
164 }
165
166 $port = intval($this->port);
167 $secure = $this->secureMethod;
168 if (empty($port)) {
169 if ($secure === 'ssl') {
170 $port = '995';
171 } else {
172 $port = '110/pop3/notls';
173 }
174 }
175
176 $serverName = trim($this->server);
177
178 //We don't add back the ssl:// or tls:// if it's already there
179 if (!empty($secure) && !strpos($serverName, '://')) {
180 $serverName = $secure.'://'.$serverName;
181 }
182
183 if (!$this->mailbox->connect($serverName, $port)) {
184 $warnings = ob_get_clean();
185 if ($this->report) {
186 $notification = [
187 'name' => 'error_connecting',
188 'removable' => 1,
189 ];
190 acym_enqueueMessage(acym_translationSprintf('ACYM_ERROR_CONNECTING', $this->server.' : '.$port), 'error', true, [$notification]);
191 }
192 if (!empty($warnings) && $this->report) {
193 acym_display($warnings, 'warning');
194 }
195
196 return false;
197 }
198
199 $login = $this->mailbox->login(trim($this->username), trim($this->password), 'USER');
200 if (empty($login) || isset($login->code)) {
201 $warnings = ob_get_clean();
202 if ($this->report) {
203 acym_enqueueMessage(acym_translationSprintf('ACYM_ERROR_LOGIN', $this->username), 'error');
204 }
205 if (!empty($warnings) && $this->report) {
206 acym_display($warnings, 'warning');
207 }
208
209 return false;
210 }
211
212 ob_end_clean();
213
214 return true;
215 }
216
217 private function connectImap(): bool
218 {
219 if (empty($this->server)) {
220 acym_enqueueMessage(acym_translation('ACYM_CONFIGURE_BOUNCE'), 'warning');
221
222 return false;
223 }
224
225 ob_start();
226 //First we reset the buffer or errors and warnings
227 $buff = $this->callImapFunction('imap_alerts', []);
228 $buff = $this->callImapFunction('imap_errors', []);
229
230 $timeout = intval($this->timeout);
231 if (!empty($timeout)) {
232 $this->callImapFunction('imap_timeout', [IMAP_OPENTIMEOUT, $timeout]);
233 }
234
235 $port = intval($this->port);
236 $secure = $this->secureMethod;
237 $protocol = $this->connectMethod;
238 $serverName = '{'.trim($this->server);
239 if (empty($port)) {
240 if ($protocol === 'imap') {
241 $port = $secure === 'ssl' ? '993' : '143';
242 } elseif ($protocol === 'pop3') {
243 $port = $secure === 'ssl' ? '995' : '110';
244 }
245 }
246
247 if (!empty($port)) {
248 $serverName .= ':'.$port;
249 }
250
251 if ($this->server === 'outlook.office365.com') {
252 $serverName .= '/imap';
253 }
254
255 //Add the secure protocol (TLS or SSL)
256 if (!empty($secure)) {
257 $serverName .= '/'.$secure;
258 }
259
260 // Test if string contains '\'
261 $email = trim($this->username);
262 if (strpos($email, '\\') !== false) {
263 [$user, $authuser] = explode('\\', $email);
264 [$x, $domain] = explode('@', $user);
265 $serverName .= '/authuser='.$user.'/user='.$authuser.'@'.$domain;
266 }
267
268 if ($this->selfSigned) {
269 $serverName .= '/novalidate-cert';
270 }
271
272 //Add the method (imap by default) ex : pop3
273 if (!empty($protocol) && $this->server !== 'outlook.office365.com') {
274 $serverName .= '/service='.$protocol;
275 }
276 $serverName .= '}';
277 if (in_array($this->server, self::HOSTS_NEEDING_OAUTH)) {
278 $this->refreshToken();
279 if (empty($this->oAuthToken)) {
280 return false;
281 }
282 $this->mailbox = imap2_open($serverName, trim($this->username), $this->oAuthToken, OP_XOAUTH2);
283 } else {
284 $this->mailbox = imap_open($serverName, trim($this->username), trim($this->password), OP_SILENT);
285 }
286 $warnings = ob_get_clean();
287
288 if ($this->report) {
289 if (!$this->mailbox) {
290 $notification = [
291 'name' => 'error_connecting',
292 'removable' => 1,
293 ];
294 acym_enqueueMessage(acym_translationSprintf('ACYM_ERROR_CONNECTING', $serverName), 'error', true, [$notification]);
295 }
296 if (!empty($warnings)) {
297 acym_enqueueMessage($warnings, 'warning');
298 }
299 }
300
301 return (bool)$this->mailbox;
302 }
303
304 public function getNBMessages(): int
305 {
306 if ($this->usePear) {
307 $this->nbMessages = intval($this->mailbox->numMsg());
308 } else {
309 $this->nbMessages = intval($this->callImapFunction('imap_num_msg', [$this->mailbox]));
310 }
311
312 return $this->nbMessages;
313 }
314
315 protected function getMessage(int $msgNB)
316 {
317 if ($this->usePear) {
318 $message = new \stdClass();
319 $message->headerString = $this->mailbox->getRawHeaders($msgNB);
320 if (empty($message->headerString)) {
321 return false;
322 }
323 } else {
324 $message = $this->callImapFunction('imap_headerinfo', [$this->mailbox, $msgNB]);
325 }
326
327 return $message;
328 }
329
330 protected function deleteMessage($msgNB): void
331 {
332 if ($this->usePear) {
333 $this->mailbox->deleteMsg($msgNB);
334 } else {
335 $this->callImapFunction('imap_delete', [$this->mailbox, $msgNB]);
336 $this->callImapFunction('imap_expunge', [$this->mailbox]);
337 }
338 }
339
340 public function close(): void
341 {
342 if ($this->usePear) {
343 $this->mailbox->disconnect();
344 } else {
345 $this->callImapFunction('imap_close', [$this->mailbox]);
346 }
347 }
348
349 public function decodeMessage(bool $attachments = false): bool
350 {
351 if ($this->usePear) {
352 return $this->decodeMessagePear($attachments);
353 } else {
354 return $this->decodeMessageImap($attachments);
355 }
356 }
357
358 private function decodeMessagePear(bool $attachments): bool
359 {
360 $this->_message->headerinfo = $this->mailbox->getParsedHeaders($this->_message->messageNB);
361 if (empty($this->_message->headerinfo['subject'])) {
362 if (empty($this->_message->headerinfo['Subject'])) {
363 return false;
364 }
365 $this->_message->headerinfo['subject'] = $this->_message->headerinfo['Subject'];
366 }
367 $this->_message->text = '';
368 $this->_message->html = $this->mailbox->getBody($this->_message->messageNB);
369
370 if (!empty($this->_message->headerinfo['content-type']) && strpos($this->_message->headerinfo['content-type'], 'boundary') !== false) {
371 $matches = [];
372 preg_match('#boundary="([^"]+)"#i', $this->_message->headerinfo['content-type'], $matches);
373
374 if (!empty($matches[1]) && strpos($this->_message->html, $matches[1]) !== false) {
375 $inlineImages = [];
376
377 $segments = explode('--'.$matches[1], $this->_message->html);
378 foreach ($segments as $segment) {
379 // Find the segment containing the html and text version
380 if (strpos($segment, 'Content-Type: text/plain') !== false) {
381 // Check if there is another boundary
382 $matches = [];
383 preg_match('#boundary="([^"]+)"#i', $segment, $matches);
384
385 if (!empty($matches[1])) {
386 $parts = explode('--'.$matches[1], $segment);
387
388 foreach ($parts as $onePart) {
389 $content = trim(preg_replace('#(charset=".*?\r\n)|Content-(Type|ID|Disposition|Transfer-Encoding):.*?\r\n#is', "", $onePart));
390
391 if (strpos($onePart, 'Content-Transfer-Encoding') !== false) {
392 preg_match('#Content-Transfer-Encoding: (.+)#i', $onePart, $encoding);
393 $encoding = trim($encoding[1]);
394 // Someone has an issue when the $encoding is equal to 'Content-Type:' only
395 if (!empty($encoding) && $encoding !== 'Content-Type:') {
396 $content = mb_convert_encoding($content, 'UTF-8', $encoding);
397 }
398 }
399
400 if (strpos($onePart, "Content-Type: text/plain") !== false) {
401 $this->_message->text .= ' '.$content;
402 }
403 if (strpos($onePart, "Content-Type: text/html") !== false) {
404 $this->_message->html .= ' '.$content;
405 }
406 }
407 } else {
408 $this->_message->html .= ' '.trim(preg_replace('#(charset=".*?\r\n)|Content-(Type|ID|Disposition|Transfer-Encoding):.*?\r\n#is', "", $segment));
409 }
410 } elseif (preg_match("#Content-Type: .*?/(png|jpg|jpeg|gif)#i", $segment) !== false) {
411 preg_match('#name="([^"]+)"#i', $segment, $filename);
412 if (empty($filename) || empty($filename[1])) {
413 continue;
414 }
415 $name = trim($filename[1]);
416 $filename = $name;
417 $extensionPos = strrpos($filename, '.');
418
419 if ($extensionPos === false) {
420 continue;
421 }
422
423 $extension = substr($filename, $extensionPos + 1);
424
425 // Make sure there is no double extension or space
426 $filename = preg_replace('#[^a-zA-Z0-9]#Uis', '_', substr($filename, 0, $extensionPos));
427
428 // Get the upload folder and create it if needed
429 $uploadFolder = str_replace(['/', '\\'], DS, acym_getFilesFolder());
430 $pathToUpload = ACYM_ROOT.trim($uploadFolder, DS).DS;
431
432 // Rename if needed
433 if (file_exists($pathToUpload.$filename.'.'.$extension)) {
434 $fileNumber = 1;
435 while (file_exists($pathToUpload.$filename.'_('.$fileNumber.').'.$extension)) {
436 $fileNumber++;
437 }
438 $filename = $filename.'_('.$fileNumber.')';
439 }
440
441 if ($attachments && preg_match("#Content-Disposition: attachment;#i", $segment) !== false) {
442 $data = trim(substr($segment, strpos($segment, "\r\n\r\n")));
443 if (strpos($segment, 'Content-Transfer-Encoding: base64') !== false) {
444 $data = base64_decode($data);
445 }
446
447 try {
448 if (acym_writeFile($pathToUpload.$filename.'.'.$extension, $data)) {
449 $attachment = new \stdClass();
450 $attachment->filename = str_replace(DS, '/', trim($uploadFolder, '\\/')).'/'.$filename.'.'.$extension;
451 $attachment->size = filesize(ACYM_ROOT.$attachment->filename);
452 $this->attachments[] = $attachment;
453 }
454 } catch (\Exception $e) {
455 $completeFilename = $filename.'.'.$extension;
456 $this->display(acym_translationSprintf('ACYM_ERROR_UPLOADING_ATTACHMENT_X_TO_X', $completeFilename, $e->getMessage()), self::MESSAGE_TYPE_ERROR);
457 }
458 } else {
459 preg_match('#Content-ID: <([^>]+)>#i', $segment, $contentID);
460 if (empty($contentID) || empty($contentID[1])) {
461 continue;
462 }
463 $contentID = trim($contentID[1]);
464
465 $data = trim(substr($segment, strpos($segment, "\r\n\r\n")));
466 if (strpos($segment, 'Content-Transfer-Encoding: base64') !== false) {
467 $data = base64_decode($data);
468 }
469
470
471 try {
472 if (acym_writeFile($pathToUpload.$filename.'.'.$extension, $data)) {
473 $inlineImages['cid:'.$contentID] = acym_rootURI().$uploadFolder.'/'.$filename.'.'.$extension;
474 }
475 } catch (\Exception $e) {
476 $this->display(acym_translationSprintf('ACYM_ERROR_UPLOAD_ATTACHMENT', $filename.'.'.$extension, $e->getMessage()), self::MESSAGE_TYPE_ERROR);
477 }
478 }
479 }
480 }
481
482 if (!empty($inlineImages)) {
483 $this->_message->html = str_replace(array_keys($inlineImages), $inlineImages, $this->_message->html);
484 }
485 }
486 }
487
488 $this->_message->subject = $this->decodeHeader($this->_message->headerinfo['subject']);
489 if (empty($this->_message->header)) {
490 $this->_message->header = new \stdClass();
491 }
492 $this->_message->header->sender_email = @$this->_message->headerinfo['return-path'];
493 if (is_array($this->_message->header->sender_email)) {
494 $this->_message->header->sender_email = reset($this->_message->header->sender_email);
495 }
496 if (!empty($this->_message->header->sender_email) && preg_match($this->detectEmail, $this->_message->header->sender_email, $results)) {
497 $this->_message->header->sender_email = $results[0];
498 }
499 $this->_message->header->sender_name = acym_stripTags(@$this->_message->headerinfo['from'] ?? '');
500 $this->_message->header->reply_to_email = $this->_message->header->sender_email;
501 $this->_message->header->reply_to_name = $this->_message->header->sender_name;
502 $this->_message->header->from_email = $this->_message->header->sender_email;
503 $this->_message->header->from_name = $this->_message->header->sender_name;
504
505 return true;
506 }
507
508 private function decodeMessageImap(bool $attachments): bool
509 {
510 $this->_message->structure = $this->callImapFunction('imap_fetchstructure', [$this->mailbox, $this->_message->messageNB]);
511
512 if (empty($this->_message->structure)) {
513 return false;
514 }
515
516 $this->_message->headerinfo = $this->callImapFunction('imap_fetchheader', [$this->mailbox, $this->_message->messageNB]);
517
518 $this->_message->html = '';
519 $this->_message->text = '';
520
521 //Multipart message : type == 1
522 if ($this->_message->structure->type == 1) {
523 $this->_message->contentType = 2;
524 if ($this->_message->structure->subtype === 'MIXED') {
525 $allParts = $this->explodeBodyMixed($this->_message->structure);
526 } else {
527 $allParts = $this->explodeBody($this->_message->structure);
528 }
529
530 $text = '';
531 foreach ($allParts as $num => $onePart) {
532 $content = $this->callImapFunction('imap_fetchbody', [$this->mailbox, $this->_message->messageNB, $num]);
533 $decodedContent = empty($content) ? '' : $this->decodeContent($content, $onePart);
534 if ($onePart->subtype === 'HTML') {
535 $this->_message->html .= $decodedContent;
536 } else {
537 if ($onePart->subtype === 'PLAIN') {
538 $text .= $decodedContent."\n";
539 }
540 $this->_message->text .= $decodedContent."\n\n- - -\n\n";
541 }
542
543 // Only for mailbox actions
544 if (!empty($this->action) && $attachments) {
545 if ((isset($onePart->parameters) && is_array($onePart->parameters)) || (isset($onePart->dparameters) && is_array($onePart->dparameters))) {
546 $this->uploadAttachment($onePart, $num);
547 } elseif (isset($onePart->parts) && is_array($onePart->parts)) {
548 foreach ($onePart->parts as $num2 => $innerPart) {
549 $this->uploadAttachment($innerPart, $num2);
550 }
551 }
552 }
553 }
554
555 if (!empty($this->action) && empty($this->_message->html)) {
556 $this->_message->html = $text."\n";
557 }
558 if (!empty($this->inlineImages)) {
559 $this->_message->html = str_replace(array_keys($this->inlineImages), $this->inlineImages, $this->_message->html);
560 }
561 } else {
562 if ($this->_message->structure->subtype === 'HTML') {
563 $this->_message->contentType = 1;
564 $this->_message->html = $this->decodeContent($this->callImapFunction('imap_body', [$this->mailbox, $this->_message->messageNB]), $this->_message->structure);
565 } else {
566 $this->_message->contentType = 0;
567 $this->_message->text = $this->decodeContent($this->callImapFunction('imap_body', [$this->mailbox, $this->_message->messageNB]), $this->_message->structure);
568 }
569 }
570
571 //Decode the subject
572 if (!empty($this->_message->subject)) {
573 $this->_message->subject = $this->decodeHeader($this->_message->subject);
574 } else {
575 $this->_message->subject = '';
576 }
577
578 $this->decodeAddressImap('sender');
579 $this->decodeAddressImap('from');
580 $this->decodeAddressImap('reply_to');
581 $this->decodeAddressImap('to');
582
583 return true;
584 }
585
586 private function uploadAttachment($attachment, $num): void
587 {
588 // It may be a real attachment, or an inline image
589 if (!isset($attachment->disposition) || (strtolower($attachment->disposition) !== 'attachment' && strtolower($attachment->disposition) !== 'inline')) {
590 return;
591 }
592
593 $name = empty($attachment->dparameters[0]->value) ? $attachment->parameters[0]->value : $attachment->dparameters[0]->value;
594 $filename = $this->decodeHeader($name);
595 $extensionPos = strrpos($filename, '.');
596
597 if ($extensionPos === false) {
598 foreach ($attachment->parameters as $key => $oneParam) {
599 if (strtolower($oneParam->attribute) === 'name') {
600 $filename = $this->decodeHeader($attachment->parameters[$key]->value);
601 $extensionPos = strrpos($filename, '.');
602 break;
603 }
604 }
605 }
606
607 // No extension => no upload
608 if ($extensionPos === false) {
609 return;
610 }
611
612 // Upload only allowed extensions in the configuration
613 $extension = substr($filename, $extensionPos + 1);
614 if (!in_array(strtolower($extension), $this->allowed_extensions)) {
615 return;
616 }
617
618 // Make sure there is no double extension or space
619 $filename = preg_replace('#[^a-zA-Z0-9]#Uis', '_', substr($filename, 0, $extensionPos));
620
621 // Get the upload folder and create it if needed
622 $uploadFolder = str_replace(['/', '\\'], DS, acym_getFilesFolder());
623 $pathToUpload = ACYM_ROOT.trim($uploadFolder, DS).DS;
624
625 // Rename if needed
626 if (file_exists($pathToUpload.$filename.'.'.$extension)) {
627 $fileNumber = 1;
628 while (file_exists($pathToUpload.$filename.'_('.$fileNumber.').'.$extension)) {
629 $fileNumber++;
630 }
631 $filename = $filename.'_('.$fileNumber.')';
632 }
633
634 $data = $this->decodeContent($this->callImapFunction('imap_fetchbody', [$this->mailbox, $this->_message->messageNB, $num]), $attachment);
635
636 try {
637 if (acym_writeFile($pathToUpload.$filename.'.'.$extension, $data)) {
638 if (
639 strtolower($attachment->disposition) !== 'attachment'
640 && !empty($attachment->id)
641 && in_array(strtolower($attachment->subtype), acym_getImageFileExtensions())
642 ) {
643 $this->inlineImages['cid:'.trim($attachment->id, '<>')] = acym_rootURI().$uploadFolder.'/'.$filename.'.'.$extension;
644 } else {
645 $attachment = new \stdClass();
646 $attachment->filename = str_replace(DS, '/', trim($uploadFolder, '\\/')).'/'.$filename.'.'.$extension;
647 $attachment->size = filesize(ACYM_ROOT.$attachment->filename);
648 $this->attachments[] = $attachment;
649 }
650 }
651 } catch (\Exception $e) {
652 $completeFilename = $filename.'.'.$extension;
653 $this->display(acym_translationSprintf('ACYM_ERROR_UPLOADING_ATTACHMENT_X_TO_X', $completeFilename, $e->getMessage()), self::MESSAGE_TYPE_ERROR);
654 }
655 }
656
657 public function handleMessages(): void
658 {
659 $maxMessages = min($this->nbMessages, $this->config->get('bounce_max', 0));
660 //500 messages maximum at once
661 if (empty($maxMessages)) {
662 $maxMessages = min($this->nbMessages, 500);
663 }
664
665 if ($this->report) {
666 if (function_exists('apache_get_modules')) {
667 $modules = apache_get_modules();
668 $this->mod_security2 = in_array('mod_security2', $modules);
669 }
670
671 /*This is to avoid the blank page... and it apparently works! ;) */
672 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Disabled to prevent blank output pages.
673 @ini_set('output_buffering', 'off');
674 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Disabled to prevent blank output pages.
675 @ini_set('zlib.output_compression', 0);
676
677
678 if (!headers_sent()) {
679 while (ob_get_level() > 0 && $this->obend++ < 3) {
680 @ob_end_flush();
681 }
682 }
683
684 ?>
685 <div style="position:fixed; top:3px;left:3px;background-color : white;border : 1px solid grey; padding : 3px;font-size:14px">
686 <?php echo esc_html(acym_translation('ACYM_BOUNCE_HANDLING')); ?>
687 : <span id="counter">0</span> / <?php echo intval($maxMessages); ?>
688 </div>
689 <script type="text/javascript" language="javascript">
690 function setCounter(val) {
691 document.getElementById('counter').innerHTML = val;
692 }
693 </script>
694 <?php
695 if (function_exists('ob_flush')) {
696 @ob_flush();
697 }
698 if (!$this->mod_security2) {
699 @flush();
700 }
701 }
702
703 //We load all the published rules
704 $rules = $this->ruleClass->getAll();
705 $rules = array_filter($rules, function ($rule) {
706 return $rule->active;
707 });
708
709 $msgNB = $maxMessages;
710 $listClass = new ListClass();
711 $this->allLists = $listClass->getAll('id');
712
713 //Exclude some email addresses...
714 $replyemail = $this->config->get('reply_email');
715 $fromemail = $this->config->get('from_email');
716 $bouncemail = $this->config->get('bounce_email');
717 $removeEmails = '#('.str_replace(['%'], ['@'], $this->config->get('bounce_username'));
718 if (!empty($bouncemail)) {
719 $removeEmails .= '|'.$bouncemail;
720 }
721 if (!empty($fromemail)) {
722 $removeEmails .= '|'.$fromemail;
723 }
724 if (!empty($replyemail)) {
725 $removeEmails .= '|'.$replyemail;
726 }
727 $removeEmails .= ')#i';
728
729 while (($msgNB > 0) && ($this->_message = $this->getMessage($msgNB))) {
730 if ($this->report) {
731 echo '<script type="text/javascript" language="javascript">setCounter('.esc_html($maxMessages - $msgNB + 1).')</script>';
732 if (function_exists('ob_flush')) {
733 @ob_flush();
734 }
735 if (!$this->mod_security2) {
736 @flush();
737 }
738 }
739 $this->_message->messageNB = $msgNB;
740 $msgNB--;
741
742 //We could not retrieve the message... we continue with the next message
743 if (!$this->decodeMessage()) {
744 $this->display(acym_translation('ACYM_ERROR_RETRIEVING_MESSAGE'), self::MESSAGE_TYPE_ERROR, $maxMessages - $this->_message->messageNB + 1);
745 continue;
746 }
747
748 if (empty($this->_message->subject)) {
749 $this->_message->subject = '';
750 }
751
752 $this->_message->analyseText = $this->_message->html.' '.$this->_message->text;
753 //We add the from in the list of possible e-mail address to check
754 if (!empty($this->_message->header->from_email)) {
755 $this->_message->analyseText .= ' '.$this->_message->header->from_email;
756 }
757 $this->display(
758 '<b>'.acym_translation('ACYM_EMAIL_SUBJECT').' : '.acym_stripTags($this->_message->subject).'</b>',
759 self::MESSAGE_TYPE_SUCCESS,
760 $maxMessages - $this->_message->messageNB + 1
761 );
762
763 //Identify the user and the e-mail...
764 preg_match('#AC([0-9]+)Y([0-9]+)BA#i', $this->_message->analyseText, $resultsVars);
765 if (!empty($resultsVars[1])) {
766 $this->_message->userid = $resultsVars[1];
767 }
768 if (!empty($resultsVars[2])) {
769 $this->_message->mailid = $resultsVars[2];
770 }
771
772 if (empty($this->_message->userid)) {
773 //We will need the e-mail itself in that case... :p
774 preg_match_all($this->detectEmail, $this->_message->analyseText, $results);
775
776 //Still no result? We try to find others
777 if (empty($results[0])) {
778 preg_match_all($this->detectEmail2, $this->_message->analyseText, $results2);
779 for ($i = 0; $i < count($results2[0]); $i++) {
780 $results[0][] = $results2[3][$i].'@'.$results2[1][$i];
781 }
782 }
783
784 if (!empty($results[0])) {
785 $alreadyChecked = [];
786 foreach ($results[0] as $oneEmail) {
787 //We will find the e-mail if it's not in the list of incorrect e-mail addresses
788 if (!preg_match($removeEmails, $oneEmail)) {
789 //We will keep this one, so we make sure it's strtolower
790 $this->_message->subemail = strtolower($oneEmail);
791 //We already checked this e-mail address... no need to try it a second time
792 if (!empty($alreadyChecked[$this->_message->subemail])) {
793 continue;
794 }
795
796 $user = $this->userClass->getOneByEmail($this->_message->subemail);
797 if (!empty($user)) $this->_message->userid = $user->id;
798
799 if (empty($this->_message->userid) && preg_match('#(&|\?)id=([0-9]+)&#ims', $this->_message->subemail, $subid)) {
800 $user = $this->userClass->getOneById($subid[2]);
801 $this->_message->userid = $user->id;
802 $this->_message->subemail = $user->email;
803 }
804
805 $alreadyChecked[$this->_message->subemail] = true;
806 if (!empty($this->_message->userid)) {
807 break;
808 }
809 }
810 }
811 }
812 }
813
814 if (empty($this->_message->mailid) && !empty($this->_message->userid)) {
815 //We can check if we have a user and only one e-mail sent for this user, it's obviously the e-mail we just sent!!
816 $this->_message->mailid = acym_loadResult(
817 'SELECT `mail_id` FROM #__acym_user_stat WHERE `user_id` = '.intval($this->_message->userid).' ORDER BY `send_date` DESC'
818 );
819 }
820
821 foreach ($rules as $oneRule) {
822 //We stop as soon as we find a good rule...
823 if ($this->handleRule($oneRule)) {
824 break;
825 }
826 }
827
828 if ($msgNB % 50 == 0) {
829 $this->userActions();
830 }
831
832 //We don't have time to finish the process? Ok, we stop it now!
833 if (!empty($this->stoptime) && time() > $this->stoptime) {
834 break;
835 }
836 }
837
838 $this->userActions();
839 $this->close();
840 }
841
842 /**
843 * Execute actions on the subscriber... and record the statistics
844 * We group them to not have performance issues
845 */
846 private function userActions(): void
847 {
848 if (!empty($this->deletedUsers)) {
849 acym_arrayToInteger($this->deletedUsers);
850 $this->userClass->delete($this->deletedUsers);
851 $this->deletedUsers = [];
852 }
853
854 if (!empty($this->blockedUsers)) {
855 acym_arrayToInteger($this->blockedUsers);
856 $this->userClass->deactivate($this->blockedUsers);
857 //We delete any other e-mail from the queue as well
858 $allUsersId = implode(',', $this->blockedUsers);
859 acym_query('DELETE FROM `#__acym_queue` WHERE `user_id` IN ('.$allUsersId.')');
860 $this->blockedUsers = [];
861 }
862
863 if (!empty($this->bounceMessages)) {
864 foreach ($this->bounceMessages as $mailid => $bouncedata) {
865 //Do we have some bounce details to update?
866 //If so, we will load the current bouncedetails and update it properly.
867 //bouncedetails is an array of bounceRule => nbTimes used
868
869 $updateBounceDetails = '';
870 if (!empty($bouncedata['bouncedetails'])) {
871 $bouncedetails = acym_loadResult('SELECT `bounce_details` FROM #__acym_mail_stat WHERE mail_id = '.intval($mailid));
872 if (!empty($bouncedetails)) {
873 $bouncedetails = unserialize($bouncedetails);
874 } else {
875 $bouncedetails = [];
876 }
877
878 foreach ($bouncedata['bouncedetails'] as $ruleName => $nbTimes) {
879 if (empty($bouncedetails[$ruleName])) {
880 $bouncedetails[$ruleName] = 0;
881 }
882 $bouncedetails[$ruleName] += $nbTimes;
883 }
884
885 $updateBounceDetails = ' , `bounce_details` = '.acym_escapeDB(serialize($bouncedetails));
886 }
887
888 if (!empty($bouncedata['userids'])) {
889 $valueInsert = [];
890 foreach ($bouncedata['ruletriggered'] as $userid => $rulename) {
891 $valueInsert[] = '('.intval($mailid).','.intval($userid).',1,'.acym_escapeDB($rulename).')';
892 }
893 acym_query(
894 'INSERT INTO #__acym_user_stat (mail_id,user_id,bounce,bounce_rule) VALUES '.implode(
895 ',',
896 $valueInsert
897 ).' ON DUPLICATE KEY UPDATE `bounce` = `bounce` + 1, bounce_rule=VALUES(bounce_rule)'
898 );
899 //We updated some profiles... let's make sure we really handle only unique bounces then and don't count it twice
900 acym_arrayToInteger($bouncedata['userids']);
901 $realUniqueBounces = acym_loadResult(
902 'SELECT COUNT(*)
903 FROM #__acym_user_stat
904 WHERE `bounce` = 1
905 AND `user_id` IN ('.implode(',', $bouncedata['userids']).')
906 AND `mail_id` = '.intval($mailid)
907 );
908 $bouncedata['nbbounces'] = $bouncedata['nbbounces'] - count($bouncedata['userids']) + $realUniqueBounces;
909 }
910
911 acym_query(
912 'UPDATE #__acym_mail_stat SET `bounce_unique` = `bounce_unique` + '.intval($bouncedata['nbbounces']).$updateBounceDetails.'
913 WHERE `mail_id` = '.intval($mailid).'
914 LIMIT 1'
915 );
916 }
917 $this->bounceMessages = [];
918 }
919 }
920
921 private function handleRule(object &$oneRule): bool
922 {
923 $regex = $oneRule->regex;
924 if (empty($regex)) {
925 return false;
926 }
927
928 //Do it based on the config of the rule...
929
930 $analyseText = '';
931 if (in_array('senderInfo', $oneRule->executed_on)) {
932 $analyseText .= ' ';
933 if (isset($this->_message->header->sender_name)) {
934 $analyseText .= $this->_message->header->sender_name;
935 }
936 if (isset($this->_message->header->sender_email)) {
937 $analyseText .= $this->_message->header->sender_email;
938 }
939 }
940
941 if (in_array('subject', $oneRule->executed_on)) {
942 $analyseText .= ' '.$this->_message->subject;
943 }
944
945 if (in_array('body', $oneRule->executed_on)) {
946 if (!empty($this->_message->html)) {
947 $analyseText .= ' '.$this->_message->html;
948 }
949 if (!empty($this->_message->text)) {
950 $analyseText .= ' '.$this->_message->text;
951 }
952 }
953
954 //Because it's easier to handle it that way... for multilines.
955 $analyseText = str_replace(["\n", "\r", "\t"], ' ', $analyseText);
956
957 if (!preg_match('#'.$regex.'#ims', $analyseText)) {
958 return false;
959 }
960
961 $message = acym_translation('ACYM_BOUNCE_RULE').' ['.acym_translation('ACYM_ID').' '.$oneRule->id.'] '.acym_translation($oneRule->name).' : ';
962 $this->errorDuringActions = false;
963 $message .= $this->actionUser($oneRule);
964 $message .= $this->actionMessage($oneRule);
965
966 $type = self::MESSAGE_TYPE_SUCCESS;
967 if ($this->errorDuringActions) {
968 $type = self::MESSAGE_TYPE_ERROR;
969 }
970
971 $this->display($message, $type);
972
973 return true;
974 }
975
976 private function actionUser(object &$oneRule): string
977 {
978 $commonListsMailUser = [];
979 $message = '';
980
981 if (empty($this->_message->userid)) {
982 $message .= 'user not identified';
983 if (!empty($this->_message->subemail)) {
984 $message .= ' ( '.$this->_message->subemail.' ) ';
985 }
986
987 return $message;
988 }
989
990 //To display nice error messages...
991 if (
992 in_array('delete_user_subscription', $oneRule->action_user)
993 || in_array('unsubscribe_user', $oneRule->action_user)
994 || in_array('subscribe_user', $oneRule->action_user)
995 ) {
996 if (empty($this->_message->subemail)) {
997 $currentUser = $this->userClass->getOneById($this->_message->userid);
998 if (!empty($currentUser->email)) {
999 $this->_message->subemail = $currentUser->email;
1000 }
1001 }
1002
1003 $userSubscription = $this->userClass->getSubscriptionStatus($this->_message->userid);
1004
1005 $bounceContent = '';
1006 if (!empty($this->_message->html)) {
1007 $bounceContent .= ' '.$this->_message->html;
1008 }
1009 if (!empty($this->_message->text)) {
1010 $bounceContent .= ' '.$this->_message->text;
1011 }
1012
1013 $emailLists = [];
1014 preg_match('#Please unsubscribe user ID \d+ from list\(s\) ([^.]+)\.#Uis', $bounceContent, $emailLists);
1015 if (!empty($emailLists[1])) {
1016 $currentEmailLists = explode(',', $emailLists[1]);
1017 foreach ($currentEmailLists as $oneListId) {
1018 if (!empty($userSubscription[$oneListId]) && $userSubscription[$oneListId]->status == 1) {
1019 $commonListsMailUser[] = $oneListId;
1020 }
1021 }
1022 }
1023
1024 if (empty($commonListsMailUser)) {
1025 $commonListsMailUser = array_keys($userSubscription);
1026 }
1027 }
1028
1029 if (empty($this->_message->subemail)) {
1030 $this->_message->subemail = $this->_message->userid;
1031 }
1032
1033
1034 //handle this rule in the stats
1035 if (!empty($this->_message->mailid)) {
1036 $mail = $this->mailClass->getOneById($this->_message->mailid);
1037 } else {
1038 $mail = null;
1039 }
1040
1041 if ($oneRule->increment_stats && !empty($this->_message->mailid) && !empty($mail)) {
1042 //Init the stats...
1043 if (empty($this->bounceMessages[$this->_message->mailid])) {
1044 $this->bounceMessages[$this->_message->mailid] = [];
1045 $this->bounceMessages[$this->_message->mailid]['nbbounces'] = 0;
1046 $this->bounceMessages[$this->_message->mailid]['userids'] = [];
1047 $this->bounceMessages[$this->_message->mailid]['bouncedetails'] = [];
1048 $this->bounceMessages[$this->_message->mailid]['ruletriggered'] = [];
1049 }
1050
1051 //Increment the global stats...
1052 $this->bounceMessages[$this->_message->mailid]['nbbounces']++;
1053
1054 //Increment the detailed stats...
1055 $ruleName = $oneRule->name.' [ID '.$oneRule->id.'] ';
1056 //We add a @ just in case the rule was not already defined...
1057 if (empty($this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName])) {
1058 $this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName] = 1;
1059 } else {
1060 $this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName]++;
1061 }
1062
1063 $user = $this->userClass->getOneById($this->_message->userid);
1064 if (!empty($this->_message->userid) && !in_array('delete_user', $oneRule->action_user) && !empty($user)) {
1065 //Increment the bounce number in the user stat table but only if we don't delete the subscriber
1066 $this->bounceMessages[$this->_message->mailid]['userids'][] = intval($this->_message->userid);
1067 $this->bounceMessages[$this->_message->mailid]['ruletriggered'][intval($this->_message->userid)] = $oneRule->name.' ['.acym_translation(
1068 'ACYM_ID'
1069 ).' '.$oneRule->id.']';
1070 }
1071 }
1072
1073 //Make sure we have enough messages to really execute this
1074 if (!empty($oneRule->execute_action_after) && $oneRule->execute_action_after > 1) {
1075 //Let's load the number of bounces the user has and then exit or not...
1076 if (empty($this->_message->mailid)) {
1077 $this->_message->mailid = 0;
1078 }
1079 $nb = intval(
1080 acym_loadResult(
1081 'SELECT COUNT(mail_id) FROM #__acym_user_stat WHERE bounce > 0 AND user_id = '.intval($this->_message->userid).' AND mail_id != '.intval(
1082 $this->_message->mailid
1083 )
1084 )
1085 ) + 1;
1086
1087 if ($nb < $oneRule->execute_action_after) {
1088 $message .= ' | '.acym_translationSprintf('ACYM_BOUNCE_RECEIVED', $nb, $this->_message->subemail).' | '.acym_translationSprintf(
1089 'ACYM_BOUNCE_MIN_EXEC',
1090 $oneRule->execute_action_after
1091 );
1092
1093 return $message;
1094 }
1095 }
1096
1097 //If we delete the subscriber, it's the last action we execute
1098 if (in_array('delete_user', $oneRule->action_user)) {
1099 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_DELETED', $this->_message->subemail);
1100 $this->deletedUsers[] = intval($this->_message->userid);
1101
1102 return $message;
1103 }
1104
1105 //We will need a default listid after...
1106 $listId = 0;
1107 if (in_array('subscribe_user', $oneRule->action_user) && !empty($oneRule->action_user['subscribe_user_list'])) {
1108 $listId = $oneRule->action_user['subscribe_user_list'];
1109
1110 $listName = empty($this->allLists[$listId]->name) ? $listId : $this->allLists[$listId]->name;
1111 $message .= ' | ';
1112 if (isset($userSubscription[$listId])) {
1113 $message .= acym_translationSprintf('ACYM_USER_X_NOT_SUBSCRIBED_TO', $this->_message->subemail, $listName);
1114 if ($userSubscription[$listId]->status == 1) {
1115 $message .= acym_translation('ACYM_USER_ALREADY_SUBSCRIBED');
1116 } elseif ($userSubscription[$listId]->status == 0) {
1117 $message .= acym_translation('ACYM_USER_ALREADY_UNSUBSCRIBED');
1118 }
1119 } else {
1120 $this->userClass->subscribe([$this->_message->userid], [$listId]);
1121 $message .= acym_translationSprintf('ACYM_USER_X_SUBSCRIBED_TO', $this->_message->subemail, $listName);
1122 }
1123 }
1124
1125 if (in_array('delete_user_subscription', $oneRule->action_user)) {
1126 $removeLists = array_diff($commonListsMailUser, [$listId]);
1127 if (!empty($removeLists)) {
1128 $listNames = [];
1129 foreach ($removeLists as $oneListId) {
1130 if (!empty($this->allLists[$oneListId]->name)) {
1131 $listNames[] = $this->allLists[$oneListId]->name;
1132 } else {
1133 $listNames[] = $oneListId;
1134 }
1135 }
1136 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_REMOVED_FROM', $this->_message->subemail, implode(', ', $listNames));
1137 $this->userClass->removeSubscription([$this->_message->userid], $removeLists);
1138 } else {
1139 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_NOT_SUBSCRIBED', $this->_message->subemail);
1140 }
1141 }
1142
1143 if (in_array('unsubscribe_user', $oneRule->action_user)) {
1144 $unsubLists = array_diff($commonListsMailUser, [$listId]);
1145 if (!empty($unsubLists)) {
1146 $listNames = [];
1147 foreach ($unsubLists as $oneListId) {
1148 if (!empty($this->allLists[$oneListId]->name)) {
1149 $listNames[] = $this->allLists[$oneListId]->name;
1150 } else {
1151 $listNames[] = $oneListId;
1152 }
1153 }
1154 $this->userClass->unsubscribe([$this->_message->userid], $unsubLists);
1155 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_UNSUBSCRIBED_FROM', $this->_message->subemail, implode(', ', $listNames));
1156 } else {
1157 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_NOT_SUBSCRIBED', $this->_message->subemail);
1158 }
1159 }
1160
1161 if (in_array('block_user', $oneRule->action_user)) {
1162 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_BLOCKED', $this->_message->subemail.' ( '.acym_translation('ACYM_ID').' '.intval($this->_message->userid).' )');
1163 $this->blockedUsers[] = intval($this->_message->userid);
1164 }
1165
1166 if (in_array('empty_queue_user', $oneRule->action_user)) {
1167 $affected = acym_query('DELETE FROM #__acym_queue WHERE user_id = '.intval($this->_message->userid));
1168 $message .= ' | '.acym_translationSprintf(
1169 'ACYM_USER_X_QUEUE',
1170 $this->_message->subemail.' ( '.acym_translation('ACYM_ID').' '.intval($this->_message->userid).' )',
1171 acym_translationSprintf('ACYM_SUCC_DELETE_ELEMENTS', $affected)
1172 );
1173 }
1174
1175 return $message;
1176 }
1177
1178 private function actionMessage(object &$oneRule): string
1179 {
1180 $message = '';
1181
1182 //Fix the rule if needed... when the forwarded user is the same as the bounce e-mail address...
1183 if (!empty($oneRule->action_message['forward_to'])) {
1184 if (
1185 strtolower($oneRule->action_message['forward_to']) === strtolower($this->config->get('bounce_username'))
1186 || strtolower($oneRule->action_message['forward_to']) === strtolower($this->config->get('bounce_email'))
1187 ) {
1188 //We don't forward it
1189 $oneRule->action_message['forward_to'] = '';
1190 //We don't delete it
1191 unset($oneRule->action_message['delete_message']);
1192 $message .= ' | '.acym_translation('ACYM_BOUNCE_NOT_FORWARD');
1193 }
1194 }
1195
1196 //Handle actions on the message itself
1197
1198 // We only delete the message if the forward didn't fail and the save in history succeeded
1199 $donotdelete = false;
1200
1201 if (in_array('save_message', $oneRule->action_message) && !empty($this->_message->userid) && !in_array('delete_user', $oneRule->action_user)) {
1202 //We have a userid, should we save the message in the database?
1203 $data = [];
1204 $data[] = 'SUBJECT::'.@htmlentities($this->_message->subject, ENT_COMPAT, 'UTF-8');
1205 $data[] = 'ACY_RULE::'.$oneRule->id.' '.$oneRule->name;
1206 $data[] = 'REPLYTO_ADDRESS::'.$this->_message->header->reply_to_name.' ( '.$this->_message->header->reply_to_email.' )';
1207 $data[] = 'FROM_ADDRESS::'.$this->_message->header->from_name.' ( '.$this->_message->header->from_email.' )';
1208 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Logging information.
1209 $data[] = @htmlentities(print_r($this->_message->headerinfo, true), ENT_COMPAT, 'UTF-8');
1210 if (empty($this->_message->mailid)) {
1211 $this->_message->mailid = 0;
1212 }
1213 try {
1214 $this->historyClass->insert($this->_message->userid, 'bounce', $data, $this->_message->mailid);
1215 $message .= ' | '.acym_translationSprintf('ACYM_BOUNCE_MESSAGE_SAVED', $this->_message->userid);
1216 } catch (\Exception $e) {
1217 $message .= ' | '.acym_translationSprintf(
1218 'ACYM_BOUNCE_SAVE_ERROR_USER_MAIL_RULE',
1219 $this->_message->userid,
1220 $this->_message->mailid,
1221 $oneRule->id,
1222 $e->getMessage()
1223 );
1224 $donotdelete = true;
1225 }
1226 }
1227
1228
1229 //We don't forward the message if it's the same mailbox!
1230 if (!empty($oneRule->action_message['forward_to'])) {
1231 //Get the forward address :
1232 $this->mailer->clearAll();
1233 $this->mailer->Subject = 'BOUNCE FORWARD : '.$this->_message->subject;
1234
1235 if (substr_count($oneRule->action_message['forward_to'], '@') > 1) {
1236 $forwardAddresses = explode(';', str_replace([';', ','], ';', $oneRule->action_message['forward_to']));
1237 } else {
1238 $forwardAddresses = [$oneRule->action_message['forward_to']];
1239 }
1240
1241 foreach ($forwardAddresses as $oneForwardAddress) {
1242 $this->mailer->AddAddress($this->mailer->cleanText($oneForwardAddress));
1243 }
1244
1245 //Add the rule at the top so we know why the message has been forwarded.
1246 $info = acym_translation('ACYM_BOUNCE_RULE').' ['.acym_translation('ACYM_ID').' '.$oneRule->id.'] '.acym_translation($oneRule->name);
1247 if (!empty($this->_message->html)) {
1248 $this->mailer->isHTML();
1249 if (!empty($this->_message->userid)) {
1250 $info .= ' ('.acym_translation('ACYM_USER_ID').': '.$this->_message->userid.')';
1251 } elseif (!empty($this->_message->subemail)) {
1252 $info .= ' ('.acym_translation('ACYM_USER_EMAIL').': '.$this->_message->subemail.')';
1253 }
1254
1255 $this->mailer->Body = $info.'<br />'.$this->_message->html;
1256 if (!empty($this->_message->text)) {
1257 $this->mailer->Body .= '<br /><br />-------<br />'.nl2br($this->_message->text);
1258 }
1259 } else {
1260 $this->mailer->isHTML(false);
1261 $this->mailer->Body = $info."\n".$this->_message->text;
1262 }
1263
1264 //We add all other extra information just in case of we could use them...
1265 //original-rcpt-to ? http://tools.ietf.org/html/rfc5965
1266 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Logging information.
1267 $this->mailer->Body .= print_r($this->_message->headerinfo, true);
1268 $replyAddress = trim(@$this->_message->header->reply_to_email, '<> ');
1269 if (!empty($replyAddress)) {
1270 if ($this->config->get('mailer_method') === \plgAcymAcymailer::SENDING_METHOD_ID) {
1271 $this->mailer->AddReplyTo($this->config->get('replyto_email'), $this->config->get('replyto_name'));
1272 } else {
1273 $this->mailer->AddReplyTo(trim($this->_message->header->reply_to_email, '<> '), $this->_message->header->reply_to_name);
1274 }
1275 }
1276
1277 $this->mailer->isForward = true;
1278 if ($this->mailer->send()) {
1279 $message .= ' | '.acym_translationSprintf('ACYM_FORWARDED_TO_X', $oneRule->action_message['forward_to']);
1280 } else {
1281 $message .= ' | '.acym_translationSprintf('ACYM_NOT_FORWARDED_TO_X', $oneRule->action_message['forward_to'], $this->mailer->reportMessage);
1282 $this->errorDuringActions = true;
1283 }
1284 }
1285
1286 if (in_array('delete_message', $oneRule->action_message) && !$this->errorDuringActions) {
1287 $message .= ' | '.acym_translation('ACYM_MESSAGE_DELETED');
1288 $this->deleteMessage($this->_message->messageNB);
1289 }
1290
1291 return $message;
1292 }
1293
1294 private function decodeAddressImap(string $type): void
1295 {
1296 $address = $type.'address';
1297 $name = $type.'_name';
1298 $email = $type.'_email';
1299 if (empty($this->_message->$type)) {
1300 return;
1301 }
1302
1303 $var = $this->_message->$type;
1304
1305 if (empty($this->_message->header)) {
1306 $this->_message->header = new \stdClass();
1307 }
1308
1309 if (!empty($this->_message->$address)) {
1310 $this->_message->header->$name = $this->_message->$address;
1311 } else {
1312 $this->_message->header->$name = $var[0]->personal;
1313 }
1314
1315 $this->_message->header->$email = $var[0]->mailbox.'@'.@$var[0]->host;
1316 }
1317
1318 /**
1319 * If num is empty then it's a message otherwise it's a send status
1320 */
1321 protected function display(string $message, int $type = self::MESSAGE_TYPE_SUCCESS, string $num = ''): void
1322 {
1323 $this->messages[] = $message;
1324
1325 if (!$this->report) return;
1326
1327 if ($type === self::MESSAGE_TYPE_SUCCESS) {
1328 $color = 'green';
1329 } elseif ($type === self::MESSAGE_TYPE_INFO) {
1330 $color = 'blue';
1331 } else {
1332 $color = 'red';
1333 }
1334
1335 if (!empty($num)) {
1336 echo '<br />'.esc_html($num).' : ';
1337 } else {
1338 echo '<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
1339 }
1340
1341 echo '<span style="color: '.esc_attr($color).'">'.esc_html($message).'</span>';
1342
1343 if (function_exists('ob_flush')) {
1344 @ob_flush();
1345 }
1346 if (!$this->mod_security2) {
1347 @flush();
1348 }
1349 }
1350
1351 public function decodeHeader(string $input): string
1352 {
1353 // Remove white space between encoded-words
1354 $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
1355 $currentCharset = false;
1356
1357 // For each encoded-word...
1358 while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
1359 $encoded = $matches[1];
1360 $charset = $matches[2];
1361 $encoding = $matches[3];
1362 $text = $matches[4];
1363
1364 switch (strtolower($encoding)) {
1365 case 'b':
1366 $text = base64_decode($text);
1367 break;
1368
1369 case 'q':
1370 $text = str_replace('_', ' ', $text);
1371 preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
1372 foreach ($matches[1] as $value) {
1373 $text = str_replace('='.$value, chr(hexdec($value)), $text);
1374 }
1375 break;
1376 }
1377 $currentCharset = $charset;
1378 $input = str_replace($encoded, $text, $input);
1379 }
1380
1381 //If we have a charset and we can handle it...
1382 if (!empty($currentCharset) && in_array($currentCharset, $this->allCharsets)) {
1383 $input = $this->encodingHelper->change($input, $currentCharset, 'UTF-8');
1384 }
1385
1386 return $input;
1387 }
1388
1389 private function explodeBodyMixed(object $struct, string $path = '1'): array
1390 {
1391 $allParts = [];
1392
1393 if (empty($struct->parts)) {
1394 return $allParts;
1395 }
1396
1397 $pathPrefix = ($struct->subtype === 'MIXED' && $path === '1') ? '' : $path.'.';
1398 foreach ($struct->parts as $i => $part) {
1399 $partPath = $pathPrefix.($i + 1);
1400 if ($part->type == 1) {
1401 $allParts = array_merge($this->explodeBodyMixed($part, $partPath), $allParts);
1402 } else {
1403 $allParts[$partPath] = $part;
1404 }
1405 }
1406
1407 return $allParts;
1408 }
1409
1410 private function explodeBody(object $struct, string $path = '0', bool $inline = false): array
1411 {
1412 $allParts = [];
1413
1414 if (empty($struct->parts)) {
1415 return $allParts;
1416 }
1417
1418 //counts real content
1419 $c = 0;
1420 foreach ($struct->parts as $part) {
1421 if ($part->type == 1) {
1422 //There are more parts....:
1423 if ($part->subtype === 'MIXED') {
1424 // refreshing current path
1425 $path = $this->incPath($path);
1426 //create a new path-id (ex.:2.0)
1427 $newpath = $path.'.0';
1428 //fetch new parts
1429 $allParts = array_merge($this->explodeBody($part, $newpath), $allParts);
1430 } else {
1431 //Alternativ / rfc / signed
1432 $newpath = $this->incPath($path);
1433 $path = $this->incPath($path);
1434 $allParts = array_merge($this->explodeBody($part, $newpath, true), $allParts);
1435 }
1436 } else {
1437 $c++;
1438 //creating new tree if this is part of a alternativ or rfc message:
1439 if ($c == 1 && $inline) {
1440 $path = $path.'.0';
1441 }
1442 //saving content:
1443 $path = $this->incPath($path);
1444 //print "<br> Content ".$path."<br>"; //debug information
1445 $allParts[$path] = $part;
1446 }
1447 }
1448
1449 return $allParts;
1450 }
1451
1452 //Increases the Path to the parts:
1453 private function incPath(string $path): string
1454 {
1455 $newPath = '';
1456 $path_elements = explode('.', $path);
1457 $limit = count($path_elements);
1458 for ($i = 0; $i < $limit; $i++) {
1459 //last element
1460 if ($i == $limit - 1) {
1461 // new Part-Number
1462 $newPath .= $path_elements[$i] + 1;
1463 } else {
1464 //rebuild "1.2.2"-Chronology
1465 $newPath .= $path_elements[$i].'.';
1466 }
1467 }
1468
1469 return $newPath;
1470 }
1471
1472 private function decodeContent(string $content, object $structure): string
1473 {
1474 $encoding = $structure->encoding;
1475
1476 //First we decode the content properly
1477 if ($encoding == 2) {
1478 $content = imap_binary($content);
1479 } elseif ($encoding == 3) {
1480 $content = base64_decode($content, true);
1481 } elseif ($encoding == 4) {
1482 $content = imap_qprint($content);
1483 }
1484
1485 // Now we convert into utf-8! only for distribution lists
1486 if (!empty($this->action)) {
1487 $charset = $this->getMailParam($structure);
1488 if (!empty($charset) && strtoupper($charset) !== 'UTF-8') {
1489 $content = $this->encodingHelper->change($content, $charset, 'UTF-8');
1490 }
1491
1492 return $content;
1493 }
1494
1495 //It can't be more than 100 000 characters, it's already plenty enough for bounce handling... we avoid embedded pictures or big attachments we could not catch before.
1496 return substr($content, 0, 100000);
1497 }
1498
1499 private function getMailParam(object $params): string
1500 {
1501 $searchIn = [];
1502
1503 if ($params->ifparameters) {
1504 $searchIn = array_merge($searchIn, $params->parameters);
1505 }
1506 if ($params->ifdparameters) {
1507 $searchIn = array_merge($searchIn, $params->dparameters);
1508 }
1509
1510 if (empty($searchIn)) {
1511 return '';
1512 }
1513
1514 foreach ($searchIn as $values) {
1515 if (strtolower($values->attribute) === 'charset') {
1516 return $values->value;
1517 }
1518 }
1519
1520 return '';
1521 }
1522
1523 public function getErrors(): array
1524 {
1525 $return = [];
1526 if (!$this->usePear) {
1527 if (!function_exists('imap_alerts')) {
1528 $return[] = 'The IMAP extension could not be loaded, please change your PHP configuration to enable it or use the pop3 method without imap extension';
1529
1530 return $return;
1531 }
1532 $alerts = $this->callImapFunction('imap_alerts');
1533 $errors = $this->callImapFunction('imap_errors');
1534 if (!empty($alerts)) {
1535 $return = array_merge($return, $alerts);
1536 }
1537 if (!empty($errors)) {
1538 $return = array_merge($return, $errors);
1539 }
1540 }
1541
1542 return $return;
1543 }
1544
1545 private function refreshToken(): void
1546 {
1547 if ($this->config->get('bounce_server') === 'imap.gmail.com') {
1548 $url = 'https://oauth2.googleapis.com/token';
1549 } else {
1550 $tenant = $this->config->get('bounce_tenant', 'consumers');
1551 $url = 'https://login.microsoftonline.com/'.$tenant.'/oauth2/v2.0/token';
1552 }
1553
1554 $oauth = new OAuth(
1555 [
1556 'sendingMethod' => 'bounce',
1557 'tokenGenerationUrl' => $url,
1558 'userName' => trim($this->config->get('bounce_username')),
1559 'clientId' => trim($this->config->get('bounce_client_id')),
1560 'clientSecret' => trim($this->config->get('bounce_client_secret')),
1561 'oauthToken' => trim($this->config->get('bounce_access_token')),
1562 'oauthTokenExpiration' => trim($this->config->get('bounce_access_token_expiration')),
1563 'refreshToken' => trim($this->config->get('bounce_refresh_token')),
1564 ]
1565 );
1566
1567 $this->oAuthToken = $oauth->getToken();
1568 }
1569
1570 private function callImapFunction(string $functionName, array $params = [])
1571 {
1572 if (in_array($this->server, self::HOSTS_NEEDING_OAUTH)) {
1573 $functionName = str_replace('imap_', 'imap2_', $functionName);
1574 }
1575
1576 if (empty($params)) {
1577 return $functionName();
1578 }
1579
1580 return $functionName(...$params);
1581 }
1582 }
1583