PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.4
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.4
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.4, at back/Helpers/BounceHelper.php

1,592 lines 64.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 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 $body = $this->callImapFunction('imap_body', [$this->mailbox, $this->_message->messageNB]);
563 $decodedBody = empty($body) ? '' : $this->decodeContent($body, $this->_message->structure);
564 if ($this->_message->structure->subtype === 'HTML') {
565 $this->_message->contentType = 1;
566 $this->_message->html = $decodedBody;
567 } else {
568 $this->_message->contentType = 0;
569 $this->_message->text = $decodedBody;
570 }
571 }
572
573 //Decode the subject
574 if (!empty($this->_message->subject)) {
575 $this->_message->subject = $this->decodeHeader($this->_message->subject);
576 } else {
577 $this->_message->subject = '';
578 }
579
580 $this->decodeAddressImap('sender');
581 $this->decodeAddressImap('from');
582 $this->decodeAddressImap('reply_to');
583 $this->decodeAddressImap('to');
584
585 return true;
586 }
587
588 private function uploadAttachment($attachment, $num): void
589 {
590 // It may be a real attachment, or an inline image
591 if (!isset($attachment->disposition) || (strtolower($attachment->disposition) !== 'attachment' && strtolower($attachment->disposition) !== 'inline')) {
592 return;
593 }
594
595 $name = empty($attachment->dparameters[0]->value) ? $attachment->parameters[0]->value : $attachment->dparameters[0]->value;
596 $filename = $this->decodeHeader($name);
597 $extensionPos = strrpos($filename, '.');
598
599 if ($extensionPos === false) {
600 foreach ($attachment->parameters as $key => $oneParam) {
601 if (strtolower($oneParam->attribute) === 'name') {
602 $filename = $this->decodeHeader($attachment->parameters[$key]->value);
603 $extensionPos = strrpos($filename, '.');
604 break;
605 }
606 }
607 }
608
609 // No extension => no upload
610 if ($extensionPos === false) {
611 return;
612 }
613
614 // Upload only allowed extensions in the configuration
615 $extension = substr($filename, $extensionPos + 1);
616 if (!in_array(strtolower($extension), $this->allowed_extensions)) {
617 return;
618 }
619
620 // Make sure there is no double extension or space
621 $filename = preg_replace('#[^a-zA-Z0-9]#Uis', '_', substr($filename, 0, $extensionPos));
622
623 // Get the upload folder and create it if needed
624 $uploadFolder = str_replace(['/', '\\'], DS, acym_getFilesFolder());
625 $pathToUpload = ACYM_ROOT.trim($uploadFolder, DS).DS;
626
627 // Rename if needed
628 if (file_exists($pathToUpload.$filename.'.'.$extension)) {
629 $fileNumber = 1;
630 while (file_exists($pathToUpload.$filename.'_('.$fileNumber.').'.$extension)) {
631 $fileNumber++;
632 }
633 $filename = $filename.'_('.$fileNumber.')';
634 }
635
636 $content = $this->callImapFunction('imap_fetchbody', [$this->mailbox, $this->_message->messageNB, $num]);
637
638 // We could not fetch the part, there is nothing to upload
639 if (empty($content)) {
640 return;
641 }
642
643 $data = $this->decodeContent($content, $attachment);
644
645 try {
646 if (acym_writeFile($pathToUpload.$filename.'.'.$extension, $data)) {
647 if (
648 strtolower($attachment->disposition) !== 'attachment'
649 && !empty($attachment->id)
650 && in_array(strtolower($attachment->subtype), acym_getImageFileExtensions())
651 ) {
652 $this->inlineImages['cid:'.trim($attachment->id, '<>')] = acym_rootURI().$uploadFolder.'/'.$filename.'.'.$extension;
653 } else {
654 $attachment = new \stdClass();
655 $attachment->filename = str_replace(DS, '/', trim($uploadFolder, '\\/')).'/'.$filename.'.'.$extension;
656 $attachment->size = filesize(ACYM_ROOT.$attachment->filename);
657 $this->attachments[] = $attachment;
658 }
659 }
660 } catch (\Exception $e) {
661 $completeFilename = $filename.'.'.$extension;
662 $this->display(acym_translationSprintf('ACYM_ERROR_UPLOADING_ATTACHMENT_X_TO_X', $completeFilename, $e->getMessage()), self::MESSAGE_TYPE_ERROR);
663 }
664 }
665
666 public function handleMessages(): void
667 {
668 $maxMessages = min($this->nbMessages, $this->config->get('bounce_max', 0));
669 //500 messages maximum at once
670 if (empty($maxMessages)) {
671 $maxMessages = min($this->nbMessages, 500);
672 }
673
674 if ($this->report) {
675 if (function_exists('apache_get_modules')) {
676 $modules = apache_get_modules();
677 $this->mod_security2 = in_array('mod_security2', $modules);
678 }
679
680 /*This is to avoid the blank page... and it apparently works! ;) */
681 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Disabled to prevent blank output pages.
682 @ini_set('output_buffering', 'off');
683 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Disabled to prevent blank output pages.
684 @ini_set('zlib.output_compression', 0);
685
686
687 if (!headers_sent()) {
688 while (ob_get_level() > 0 && $this->obend++ < 3) {
689 @ob_end_flush();
690 }
691 }
692
693 ?>
694 <div style="position:fixed; top:3px;left:3px;background-color : white;border : 1px solid grey; padding : 3px;font-size:14px">
695 <?php echo esc_html(acym_translation('ACYM_BOUNCE_HANDLING')); ?>
696 : <span id="counter">0</span> / <?php echo intval($maxMessages); ?>
697 </div>
698 <script type="text/javascript" language="javascript">
699 function setCounter(val) {
700 document.getElementById('counter').innerHTML = val;
701 }
702 </script>
703 <?php
704 if (function_exists('ob_flush')) {
705 @ob_flush();
706 }
707 if (!$this->mod_security2) {
708 @flush();
709 }
710 }
711
712 //We load all the published rules
713 $rules = $this->ruleClass->getAll();
714 $rules = array_filter($rules, function ($rule) {
715 return $rule->active;
716 });
717
718 $msgNB = $maxMessages;
719 $listClass = new ListClass();
720 $this->allLists = $listClass->getAll('id');
721
722 //Exclude some email addresses...
723 $replyemail = $this->config->get('reply_email');
724 $fromemail = $this->config->get('from_email');
725 $bouncemail = $this->config->get('bounce_email');
726 $removeEmails = '#('.str_replace(['%'], ['@'], $this->config->get('bounce_username'));
727 if (!empty($bouncemail)) {
728 $removeEmails .= '|'.$bouncemail;
729 }
730 if (!empty($fromemail)) {
731 $removeEmails .= '|'.$fromemail;
732 }
733 if (!empty($replyemail)) {
734 $removeEmails .= '|'.$replyemail;
735 }
736 $removeEmails .= ')#i';
737
738 while (($msgNB > 0) && ($this->_message = $this->getMessage($msgNB))) {
739 if ($this->report) {
740 echo '<script type="text/javascript" language="javascript">setCounter('.esc_html($maxMessages - $msgNB + 1).')</script>';
741 if (function_exists('ob_flush')) {
742 @ob_flush();
743 }
744 if (!$this->mod_security2) {
745 @flush();
746 }
747 }
748 $this->_message->messageNB = $msgNB;
749 $msgNB--;
750
751 //We could not retrieve the message... we continue with the next message
752 if (!$this->decodeMessage()) {
753 $this->display(acym_translation('ACYM_ERROR_RETRIEVING_MESSAGE'), self::MESSAGE_TYPE_ERROR, $maxMessages - $this->_message->messageNB + 1);
754 continue;
755 }
756
757 if (empty($this->_message->subject)) {
758 $this->_message->subject = '';
759 }
760
761 $this->_message->analyseText = $this->_message->html.' '.$this->_message->text;
762 //We add the from in the list of possible e-mail address to check
763 if (!empty($this->_message->header->from_email)) {
764 $this->_message->analyseText .= ' '.$this->_message->header->from_email;
765 }
766 $this->display(
767 '<b>'.acym_translation('ACYM_EMAIL_SUBJECT').' : '.acym_stripTags($this->_message->subject).'</b>',
768 self::MESSAGE_TYPE_SUCCESS,
769 $maxMessages - $this->_message->messageNB + 1
770 );
771
772 //Identify the user and the e-mail...
773 preg_match('#AC([0-9]+)Y([0-9]+)BA#i', $this->_message->analyseText, $resultsVars);
774 if (!empty($resultsVars[1])) {
775 $this->_message->userid = $resultsVars[1];
776 }
777 if (!empty($resultsVars[2])) {
778 $this->_message->mailid = $resultsVars[2];
779 }
780
781 if (empty($this->_message->userid)) {
782 //We will need the e-mail itself in that case... :p
783 preg_match_all($this->detectEmail, $this->_message->analyseText, $results);
784
785 //Still no result? We try to find others
786 if (empty($results[0])) {
787 preg_match_all($this->detectEmail2, $this->_message->analyseText, $results2);
788 for ($i = 0; $i < count($results2[0]); $i++) {
789 $results[0][] = $results2[3][$i].'@'.$results2[1][$i];
790 }
791 }
792
793 if (!empty($results[0])) {
794 $alreadyChecked = [];
795 foreach ($results[0] as $oneEmail) {
796 //We will find the e-mail if it's not in the list of incorrect e-mail addresses
797 if (!preg_match($removeEmails, $oneEmail)) {
798 //We will keep this one, so we make sure it's strtolower
799 $this->_message->subemail = strtolower($oneEmail);
800 //We already checked this e-mail address... no need to try it a second time
801 if (!empty($alreadyChecked[$this->_message->subemail])) {
802 continue;
803 }
804
805 $user = $this->userClass->getOneByEmail($this->_message->subemail);
806 if (!empty($user)) $this->_message->userid = $user->id;
807
808 if (empty($this->_message->userid) && preg_match('#(&|\?)id=([0-9]+)&#ims', $this->_message->subemail, $subid)) {
809 $user = $this->userClass->getOneById($subid[2]);
810 $this->_message->userid = $user->id;
811 $this->_message->subemail = $user->email;
812 }
813
814 $alreadyChecked[$this->_message->subemail] = true;
815 if (!empty($this->_message->userid)) {
816 break;
817 }
818 }
819 }
820 }
821 }
822
823 if (empty($this->_message->mailid) && !empty($this->_message->userid)) {
824 //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!!
825 $this->_message->mailid = acym_loadResult(
826 'SELECT `mail_id` FROM #__acym_user_stat WHERE `user_id` = '.intval($this->_message->userid).' ORDER BY `send_date` DESC'
827 );
828 }
829
830 foreach ($rules as $oneRule) {
831 //We stop as soon as we find a good rule...
832 if ($this->handleRule($oneRule)) {
833 break;
834 }
835 }
836
837 if ($msgNB % 50 == 0) {
838 $this->userActions();
839 }
840
841 //We don't have time to finish the process? Ok, we stop it now!
842 if (!empty($this->stoptime) && time() > $this->stoptime) {
843 break;
844 }
845 }
846
847 $this->userActions();
848 $this->close();
849 }
850
851 /**
852 * Execute actions on the subscriber... and record the statistics
853 * We group them to not have performance issues
854 */
855 private function userActions(): void
856 {
857 if (!empty($this->deletedUsers)) {
858 acym_arrayToInteger($this->deletedUsers);
859 $this->userClass->delete($this->deletedUsers);
860 $this->deletedUsers = [];
861 }
862
863 if (!empty($this->blockedUsers)) {
864 acym_arrayToInteger($this->blockedUsers);
865 $this->userClass->deactivate($this->blockedUsers);
866 //We delete any other e-mail from the queue as well
867 $allUsersId = implode(',', $this->blockedUsers);
868 acym_query('DELETE FROM `#__acym_queue` WHERE `user_id` IN ('.$allUsersId.')');
869 $this->blockedUsers = [];
870 }
871
872 if (!empty($this->bounceMessages)) {
873 foreach ($this->bounceMessages as $mailid => $bouncedata) {
874 //Do we have some bounce details to update?
875 //If so, we will load the current bouncedetails and update it properly.
876 //bouncedetails is an array of bounceRule => nbTimes used
877
878 $updateBounceDetails = '';
879 if (!empty($bouncedata['bouncedetails'])) {
880 $bouncedetails = acym_loadResult('SELECT `bounce_details` FROM #__acym_mail_stat WHERE mail_id = '.intval($mailid));
881 if (!empty($bouncedetails)) {
882 $bouncedetails = unserialize($bouncedetails);
883 } else {
884 $bouncedetails = [];
885 }
886
887 foreach ($bouncedata['bouncedetails'] as $ruleName => $nbTimes) {
888 if (empty($bouncedetails[$ruleName])) {
889 $bouncedetails[$ruleName] = 0;
890 }
891 $bouncedetails[$ruleName] += $nbTimes;
892 }
893
894 $updateBounceDetails = ' , `bounce_details` = '.acym_escapeDB(serialize($bouncedetails));
895 }
896
897 if (!empty($bouncedata['userids'])) {
898 $valueInsert = [];
899 foreach ($bouncedata['ruletriggered'] as $userid => $rulename) {
900 $valueInsert[] = '('.intval($mailid).','.intval($userid).',1,'.acym_escapeDB($rulename).')';
901 }
902 acym_query(
903 'INSERT INTO #__acym_user_stat (mail_id,user_id,bounce,bounce_rule) VALUES '.implode(
904 ',',
905 $valueInsert
906 ).' ON DUPLICATE KEY UPDATE `bounce` = `bounce` + 1, bounce_rule=VALUES(bounce_rule)'
907 );
908 //We updated some profiles... let's make sure we really handle only unique bounces then and don't count it twice
909 acym_arrayToInteger($bouncedata['userids']);
910 $realUniqueBounces = acym_loadResult(
911 'SELECT COUNT(*)
912 FROM #__acym_user_stat
913 WHERE `bounce` = 1
914 AND `user_id` IN ('.implode(',', $bouncedata['userids']).')
915 AND `mail_id` = '.intval($mailid)
916 );
917 $bouncedata['nbbounces'] = $bouncedata['nbbounces'] - count($bouncedata['userids']) + $realUniqueBounces;
918 }
919
920 acym_query(
921 'UPDATE #__acym_mail_stat SET `bounce_unique` = `bounce_unique` + '.intval($bouncedata['nbbounces']).$updateBounceDetails.'
922 WHERE `mail_id` = '.intval($mailid).'
923 LIMIT 1'
924 );
925 }
926 $this->bounceMessages = [];
927 }
928 }
929
930 private function handleRule(object &$oneRule): bool
931 {
932 $regex = $oneRule->regex;
933 if (empty($regex)) {
934 return false;
935 }
936
937 //Do it based on the config of the rule...
938
939 $analyseText = '';
940 if (in_array('senderInfo', $oneRule->executed_on)) {
941 $analyseText .= ' ';
942 if (isset($this->_message->header->sender_name)) {
943 $analyseText .= $this->_message->header->sender_name;
944 }
945 if (isset($this->_message->header->sender_email)) {
946 $analyseText .= $this->_message->header->sender_email;
947 }
948 }
949
950 if (in_array('subject', $oneRule->executed_on)) {
951 $analyseText .= ' '.$this->_message->subject;
952 }
953
954 if (in_array('body', $oneRule->executed_on)) {
955 if (!empty($this->_message->html)) {
956 $analyseText .= ' '.$this->_message->html;
957 }
958 if (!empty($this->_message->text)) {
959 $analyseText .= ' '.$this->_message->text;
960 }
961 }
962
963 //Because it's easier to handle it that way... for multilines.
964 $analyseText = str_replace(["\n", "\r", "\t"], ' ', $analyseText);
965
966 if (!preg_match('#'.$regex.'#ims', $analyseText)) {
967 return false;
968 }
969
970 $message = acym_translation('ACYM_BOUNCE_RULE').' ['.acym_translation('ACYM_ID').' '.$oneRule->id.'] '.acym_translation($oneRule->name).' : ';
971 $this->errorDuringActions = false;
972 $message .= $this->actionUser($oneRule);
973 $message .= $this->actionMessage($oneRule);
974
975 $type = self::MESSAGE_TYPE_SUCCESS;
976 if ($this->errorDuringActions) {
977 $type = self::MESSAGE_TYPE_ERROR;
978 }
979
980 $this->display($message, $type);
981
982 return true;
983 }
984
985 private function actionUser(object &$oneRule): string
986 {
987 $commonListsMailUser = [];
988 $message = '';
989
990 if (empty($this->_message->userid)) {
991 $message .= 'user not identified';
992 if (!empty($this->_message->subemail)) {
993 $message .= ' ( '.$this->_message->subemail.' ) ';
994 }
995
996 return $message;
997 }
998
999 //To display nice error messages...
1000 if (
1001 in_array('delete_user_subscription', $oneRule->action_user)
1002 || in_array('unsubscribe_user', $oneRule->action_user)
1003 || in_array('subscribe_user', $oneRule->action_user)
1004 ) {
1005 if (empty($this->_message->subemail)) {
1006 $currentUser = $this->userClass->getOneById($this->_message->userid);
1007 if (!empty($currentUser->email)) {
1008 $this->_message->subemail = $currentUser->email;
1009 }
1010 }
1011
1012 $userSubscription = $this->userClass->getSubscriptionStatus($this->_message->userid);
1013
1014 $bounceContent = '';
1015 if (!empty($this->_message->html)) {
1016 $bounceContent .= ' '.$this->_message->html;
1017 }
1018 if (!empty($this->_message->text)) {
1019 $bounceContent .= ' '.$this->_message->text;
1020 }
1021
1022 $emailLists = [];
1023 preg_match('#Please unsubscribe user ID \d+ from list\(s\) ([^.]+)\.#Uis', $bounceContent, $emailLists);
1024 if (!empty($emailLists[1])) {
1025 $currentEmailLists = explode(',', $emailLists[1]);
1026 foreach ($currentEmailLists as $oneListId) {
1027 if (!empty($userSubscription[$oneListId]) && $userSubscription[$oneListId]->status == 1) {
1028 $commonListsMailUser[] = $oneListId;
1029 }
1030 }
1031 }
1032
1033 if (empty($commonListsMailUser)) {
1034 $commonListsMailUser = array_keys($userSubscription);
1035 }
1036 }
1037
1038 if (empty($this->_message->subemail)) {
1039 $this->_message->subemail = $this->_message->userid;
1040 }
1041
1042
1043 //handle this rule in the stats
1044 if (!empty($this->_message->mailid)) {
1045 $mail = $this->mailClass->getOneById($this->_message->mailid);
1046 } else {
1047 $mail = null;
1048 }
1049
1050 if ($oneRule->increment_stats && !empty($this->_message->mailid) && !empty($mail)) {
1051 //Init the stats...
1052 if (empty($this->bounceMessages[$this->_message->mailid])) {
1053 $this->bounceMessages[$this->_message->mailid] = [];
1054 $this->bounceMessages[$this->_message->mailid]['nbbounces'] = 0;
1055 $this->bounceMessages[$this->_message->mailid]['userids'] = [];
1056 $this->bounceMessages[$this->_message->mailid]['bouncedetails'] = [];
1057 $this->bounceMessages[$this->_message->mailid]['ruletriggered'] = [];
1058 }
1059
1060 //Increment the global stats...
1061 $this->bounceMessages[$this->_message->mailid]['nbbounces']++;
1062
1063 //Increment the detailed stats...
1064 $ruleName = $oneRule->name.' [ID '.$oneRule->id.'] ';
1065 //We add a @ just in case the rule was not already defined...
1066 if (empty($this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName])) {
1067 $this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName] = 1;
1068 } else {
1069 $this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName]++;
1070 }
1071
1072 $user = $this->userClass->getOneById($this->_message->userid);
1073 if (!empty($this->_message->userid) && !in_array('delete_user', $oneRule->action_user) && !empty($user)) {
1074 //Increment the bounce number in the user stat table but only if we don't delete the subscriber
1075 $this->bounceMessages[$this->_message->mailid]['userids'][] = intval($this->_message->userid);
1076 $this->bounceMessages[$this->_message->mailid]['ruletriggered'][intval($this->_message->userid)] = $oneRule->name.' ['.acym_translation(
1077 'ACYM_ID'
1078 ).' '.$oneRule->id.']';
1079 }
1080 }
1081
1082 //Make sure we have enough messages to really execute this
1083 if (!empty($oneRule->execute_action_after) && $oneRule->execute_action_after > 1) {
1084 //Let's load the number of bounces the user has and then exit or not...
1085 if (empty($this->_message->mailid)) {
1086 $this->_message->mailid = 0;
1087 }
1088 $nb = intval(
1089 acym_loadResult(
1090 'SELECT COUNT(mail_id) FROM #__acym_user_stat WHERE bounce > 0 AND user_id = '.intval($this->_message->userid).' AND mail_id != '.intval(
1091 $this->_message->mailid
1092 )
1093 )
1094 ) + 1;
1095
1096 if ($nb < $oneRule->execute_action_after) {
1097 $message .= ' | '.acym_translationSprintf('ACYM_BOUNCE_RECEIVED', $nb, $this->_message->subemail).' | '.acym_translationSprintf(
1098 'ACYM_BOUNCE_MIN_EXEC',
1099 $oneRule->execute_action_after
1100 );
1101
1102 return $message;
1103 }
1104 }
1105
1106 //If we delete the subscriber, it's the last action we execute
1107 if (in_array('delete_user', $oneRule->action_user)) {
1108 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_DELETED', $this->_message->subemail);
1109 $this->deletedUsers[] = intval($this->_message->userid);
1110
1111 return $message;
1112 }
1113
1114 //We will need a default listid after...
1115 $listId = 0;
1116 if (in_array('subscribe_user', $oneRule->action_user) && !empty($oneRule->action_user['subscribe_user_list'])) {
1117 $listId = $oneRule->action_user['subscribe_user_list'];
1118
1119 $listName = empty($this->allLists[$listId]->name) ? $listId : $this->allLists[$listId]->name;
1120 $message .= ' | ';
1121 if (isset($userSubscription[$listId])) {
1122 $message .= acym_translationSprintf('ACYM_USER_X_NOT_SUBSCRIBED_TO', $this->_message->subemail, $listName);
1123 if ($userSubscription[$listId]->status == 1) {
1124 $message .= acym_translation('ACYM_USER_ALREADY_SUBSCRIBED');
1125 } elseif ($userSubscription[$listId]->status == 0) {
1126 $message .= acym_translation('ACYM_USER_ALREADY_UNSUBSCRIBED');
1127 }
1128 } else {
1129 $this->userClass->subscribe([$this->_message->userid], [$listId]);
1130 $message .= acym_translationSprintf('ACYM_USER_X_SUBSCRIBED_TO', $this->_message->subemail, $listName);
1131 }
1132 }
1133
1134 if (in_array('delete_user_subscription', $oneRule->action_user)) {
1135 $removeLists = array_diff($commonListsMailUser, [$listId]);
1136 if (!empty($removeLists)) {
1137 $listNames = [];
1138 foreach ($removeLists as $oneListId) {
1139 if (!empty($this->allLists[$oneListId]->name)) {
1140 $listNames[] = $this->allLists[$oneListId]->name;
1141 } else {
1142 $listNames[] = $oneListId;
1143 }
1144 }
1145 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_REMOVED_FROM', $this->_message->subemail, implode(', ', $listNames));
1146 $this->userClass->removeSubscription([$this->_message->userid], $removeLists);
1147 } else {
1148 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_NOT_SUBSCRIBED', $this->_message->subemail);
1149 }
1150 }
1151
1152 if (in_array('unsubscribe_user', $oneRule->action_user)) {
1153 $unsubLists = array_diff($commonListsMailUser, [$listId]);
1154 if (!empty($unsubLists)) {
1155 $listNames = [];
1156 foreach ($unsubLists as $oneListId) {
1157 if (!empty($this->allLists[$oneListId]->name)) {
1158 $listNames[] = $this->allLists[$oneListId]->name;
1159 } else {
1160 $listNames[] = $oneListId;
1161 }
1162 }
1163 $this->userClass->unsubscribe([$this->_message->userid], $unsubLists);
1164 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_UNSUBSCRIBED_FROM', $this->_message->subemail, implode(', ', $listNames));
1165 } else {
1166 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_NOT_SUBSCRIBED', $this->_message->subemail);
1167 }
1168 }
1169
1170 if (in_array('block_user', $oneRule->action_user)) {
1171 $message .= ' | '.acym_translationSprintf('ACYM_USER_X_BLOCKED', $this->_message->subemail.' ( '.acym_translation('ACYM_ID').' '.intval($this->_message->userid).' )');
1172 $this->blockedUsers[] = intval($this->_message->userid);
1173 }
1174
1175 if (in_array('empty_queue_user', $oneRule->action_user)) {
1176 $affected = acym_query('DELETE FROM #__acym_queue WHERE user_id = '.intval($this->_message->userid));
1177 $message .= ' | '.acym_translationSprintf(
1178 'ACYM_USER_X_QUEUE',
1179 $this->_message->subemail.' ( '.acym_translation('ACYM_ID').' '.intval($this->_message->userid).' )',
1180 acym_translationSprintf('ACYM_SUCC_DELETE_ELEMENTS', $affected)
1181 );
1182 }
1183
1184 return $message;
1185 }
1186
1187 private function actionMessage(object &$oneRule): string
1188 {
1189 $message = '';
1190
1191 //Fix the rule if needed... when the forwarded user is the same as the bounce e-mail address...
1192 if (!empty($oneRule->action_message['forward_to'])) {
1193 if (
1194 strtolower($oneRule->action_message['forward_to']) === strtolower($this->config->get('bounce_username'))
1195 || strtolower($oneRule->action_message['forward_to']) === strtolower($this->config->get('bounce_email'))
1196 ) {
1197 //We don't forward it
1198 $oneRule->action_message['forward_to'] = '';
1199 //We don't delete it
1200 unset($oneRule->action_message['delete_message']);
1201 $message .= ' | '.acym_translation('ACYM_BOUNCE_NOT_FORWARD');
1202 }
1203 }
1204
1205 //Handle actions on the message itself
1206
1207 // We only delete the message if the forward didn't fail and the save in history succeeded
1208 $donotdelete = false;
1209
1210 if (in_array('save_message', $oneRule->action_message) && !empty($this->_message->userid) && !in_array('delete_user', $oneRule->action_user)) {
1211 //We have a userid, should we save the message in the database?
1212 $data = [];
1213 $data[] = 'SUBJECT::'.@htmlentities($this->_message->subject, ENT_COMPAT, 'UTF-8');
1214 $data[] = 'ACY_RULE::'.$oneRule->id.' '.$oneRule->name;
1215 $data[] = 'REPLYTO_ADDRESS::'.$this->_message->header->reply_to_name.' ( '.$this->_message->header->reply_to_email.' )';
1216 $data[] = 'FROM_ADDRESS::'.$this->_message->header->from_name.' ( '.$this->_message->header->from_email.' )';
1217 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Logging information.
1218 $data[] = @htmlentities(print_r($this->_message->headerinfo, true), ENT_COMPAT, 'UTF-8');
1219 if (empty($this->_message->mailid)) {
1220 $this->_message->mailid = 0;
1221 }
1222 try {
1223 $this->historyClass->insert($this->_message->userid, 'bounce', $data, $this->_message->mailid);
1224 $message .= ' | '.acym_translationSprintf('ACYM_BOUNCE_MESSAGE_SAVED', $this->_message->userid);
1225 } catch (\Exception $e) {
1226 $message .= ' | '.acym_translationSprintf(
1227 'ACYM_BOUNCE_SAVE_ERROR_USER_MAIL_RULE',
1228 $this->_message->userid,
1229 $this->_message->mailid,
1230 $oneRule->id,
1231 $e->getMessage()
1232 );
1233 $donotdelete = true;
1234 }
1235 }
1236
1237
1238 //We don't forward the message if it's the same mailbox!
1239 if (!empty($oneRule->action_message['forward_to'])) {
1240 //Get the forward address :
1241 $this->mailer->clearAll();
1242 $this->mailer->Subject = 'BOUNCE FORWARD : '.$this->_message->subject;
1243
1244 if (substr_count($oneRule->action_message['forward_to'], '@') > 1) {
1245 $forwardAddresses = explode(';', str_replace([';', ','], ';', $oneRule->action_message['forward_to']));
1246 } else {
1247 $forwardAddresses = [$oneRule->action_message['forward_to']];
1248 }
1249
1250 foreach ($forwardAddresses as $oneForwardAddress) {
1251 $this->mailer->AddAddress($this->mailer->cleanText($oneForwardAddress));
1252 }
1253
1254 //Add the rule at the top so we know why the message has been forwarded.
1255 $info = acym_translation('ACYM_BOUNCE_RULE').' ['.acym_translation('ACYM_ID').' '.$oneRule->id.'] '.acym_translation($oneRule->name);
1256 if (!empty($this->_message->html)) {
1257 $this->mailer->isHTML();
1258 if (!empty($this->_message->userid)) {
1259 $info .= ' ('.acym_translation('ACYM_USER_ID').': '.$this->_message->userid.')';
1260 } elseif (!empty($this->_message->subemail)) {
1261 $info .= ' ('.acym_translation('ACYM_USER_EMAIL').': '.$this->_message->subemail.')';
1262 }
1263
1264 $this->mailer->Body = $info.'<br />'.$this->_message->html;
1265 if (!empty($this->_message->text)) {
1266 $this->mailer->Body .= '<br /><br />-------<br />'.nl2br($this->_message->text);
1267 }
1268 } else {
1269 $this->mailer->isHTML(false);
1270 $this->mailer->Body = $info."\n".$this->_message->text;
1271 }
1272
1273 //We add all other extra information just in case of we could use them...
1274 //original-rcpt-to ? http://tools.ietf.org/html/rfc5965
1275 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Logging information.
1276 $this->mailer->Body .= print_r($this->_message->headerinfo, true);
1277 $replyAddress = trim(@$this->_message->header->reply_to_email, '<> ');
1278 if (!empty($replyAddress)) {
1279 if ($this->config->get('mailer_method') === \plgAcymAcymailer::SENDING_METHOD_ID) {
1280 $this->mailer->AddReplyTo($this->config->get('replyto_email'), $this->config->get('replyto_name'));
1281 } else {
1282 $this->mailer->AddReplyTo(trim($this->_message->header->reply_to_email, '<> '), $this->_message->header->reply_to_name);
1283 }
1284 }
1285
1286 $this->mailer->isForward = true;
1287 if ($this->mailer->send()) {
1288 $message .= ' | '.acym_translationSprintf('ACYM_FORWARDED_TO_X', $oneRule->action_message['forward_to']);
1289 } else {
1290 $message .= ' | '.acym_translationSprintf('ACYM_NOT_FORWARDED_TO_X', $oneRule->action_message['forward_to'], $this->mailer->reportMessage);
1291 $this->errorDuringActions = true;
1292 }
1293 }
1294
1295 if (in_array('delete_message', $oneRule->action_message) && !$this->errorDuringActions) {
1296 $message .= ' | '.acym_translation('ACYM_MESSAGE_DELETED');
1297 $this->deleteMessage($this->_message->messageNB);
1298 }
1299
1300 return $message;
1301 }
1302
1303 private function decodeAddressImap(string $type): void
1304 {
1305 $address = $type.'address';
1306 $name = $type.'_name';
1307 $email = $type.'_email';
1308 if (empty($this->_message->$type)) {
1309 return;
1310 }
1311
1312 $var = $this->_message->$type;
1313
1314 if (empty($this->_message->header)) {
1315 $this->_message->header = new \stdClass();
1316 }
1317
1318 if (!empty($this->_message->$address)) {
1319 $this->_message->header->$name = $this->_message->$address;
1320 } else {
1321 $this->_message->header->$name = $var[0]->personal;
1322 }
1323
1324 $this->_message->header->$email = $var[0]->mailbox.'@'.@$var[0]->host;
1325 }
1326
1327 /**
1328 * If num is empty then it's a message otherwise it's a send status
1329 */
1330 protected function display(string $message, int $type = self::MESSAGE_TYPE_SUCCESS, string $num = ''): void
1331 {
1332 $this->messages[] = $message;
1333
1334 if (!$this->report) return;
1335
1336 if ($type === self::MESSAGE_TYPE_SUCCESS) {
1337 $color = 'green';
1338 } elseif ($type === self::MESSAGE_TYPE_INFO) {
1339 $color = 'blue';
1340 } else {
1341 $color = 'red';
1342 }
1343
1344 if (!empty($num)) {
1345 echo '<br />'.esc_html($num).' : ';
1346 } else {
1347 echo '<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
1348 }
1349
1350 echo '<span style="color: '.esc_attr($color).'">'.esc_html($message).'</span>';
1351
1352 if (function_exists('ob_flush')) {
1353 @ob_flush();
1354 }
1355 if (!$this->mod_security2) {
1356 @flush();
1357 }
1358 }
1359
1360 public function decodeHeader(string $input): string
1361 {
1362 // Remove white space between encoded-words
1363 $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
1364 $currentCharset = false;
1365
1366 // For each encoded-word...
1367 while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
1368 $encoded = $matches[1];
1369 $charset = $matches[2];
1370 $encoding = $matches[3];
1371 $text = $matches[4];
1372
1373 switch (strtolower($encoding)) {
1374 case 'b':
1375 $text = base64_decode($text);
1376 break;
1377
1378 case 'q':
1379 $text = str_replace('_', ' ', $text);
1380 preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
1381 foreach ($matches[1] as $value) {
1382 $text = str_replace('='.$value, chr(hexdec($value)), $text);
1383 }
1384 break;
1385 }
1386 $currentCharset = $charset;
1387 $input = str_replace($encoded, $text, $input);
1388 }
1389
1390 //If we have a charset and we can handle it...
1391 if (!empty($currentCharset) && in_array($currentCharset, $this->allCharsets)) {
1392 $input = $this->encodingHelper->change($input, $currentCharset, 'UTF-8');
1393 }
1394
1395 return $input;
1396 }
1397
1398 private function explodeBodyMixed(object $struct, string $path = '1'): array
1399 {
1400 $allParts = [];
1401
1402 if (empty($struct->parts)) {
1403 return $allParts;
1404 }
1405
1406 $pathPrefix = ($struct->subtype === 'MIXED' && $path === '1') ? '' : $path.'.';
1407 foreach ($struct->parts as $i => $part) {
1408 $partPath = $pathPrefix.($i + 1);
1409 if ($part->type == 1) {
1410 $allParts = array_merge($this->explodeBodyMixed($part, $partPath), $allParts);
1411 } else {
1412 $allParts[$partPath] = $part;
1413 }
1414 }
1415
1416 return $allParts;
1417 }
1418
1419 private function explodeBody(object $struct, string $path = '0', bool $inline = false): array
1420 {
1421 $allParts = [];
1422
1423 if (empty($struct->parts)) {
1424 return $allParts;
1425 }
1426
1427 //counts real content
1428 $c = 0;
1429 foreach ($struct->parts as $part) {
1430 if ($part->type == 1) {
1431 //There are more parts....:
1432 if ($part->subtype === 'MIXED') {
1433 // refreshing current path
1434 $path = $this->incPath($path);
1435 //create a new path-id (ex.:2.0)
1436 $newpath = $path.'.0';
1437 //fetch new parts
1438 $allParts = array_merge($this->explodeBody($part, $newpath), $allParts);
1439 } else {
1440 //Alternativ / rfc / signed
1441 $newpath = $this->incPath($path);
1442 $path = $this->incPath($path);
1443 $allParts = array_merge($this->explodeBody($part, $newpath, true), $allParts);
1444 }
1445 } else {
1446 $c++;
1447 //creating new tree if this is part of a alternativ or rfc message:
1448 if ($c == 1 && $inline) {
1449 $path = $path.'.0';
1450 }
1451 //saving content:
1452 $path = $this->incPath($path);
1453 //print "<br> Content ".$path."<br>"; //debug information
1454 $allParts[$path] = $part;
1455 }
1456 }
1457
1458 return $allParts;
1459 }
1460
1461 //Increases the Path to the parts:
1462 private function incPath(string $path): string
1463 {
1464 $newPath = '';
1465 $path_elements = explode('.', $path);
1466 $limit = count($path_elements);
1467 for ($i = 0; $i < $limit; $i++) {
1468 //last element
1469 if ($i == $limit - 1) {
1470 // new Part-Number
1471 $newPath .= $path_elements[$i] + 1;
1472 } else {
1473 //rebuild "1.2.2"-Chronology
1474 $newPath .= $path_elements[$i].'.';
1475 }
1476 }
1477
1478 return $newPath;
1479 }
1480
1481 private function decodeContent(string $content, object $structure): string
1482 {
1483 $encoding = $structure->encoding;
1484
1485 //First we decode the content properly
1486 if ($encoding == 2) {
1487 $content = imap_binary($content);
1488 } elseif ($encoding == 3) {
1489 $content = base64_decode($content, true);
1490 } elseif ($encoding == 4) {
1491 $content = imap_qprint($content);
1492 }
1493
1494 // Now we convert into utf-8! only for distribution lists
1495 if (!empty($this->action)) {
1496 $charset = $this->getMailParam($structure);
1497 if (!empty($charset) && strtoupper($charset) !== 'UTF-8') {
1498 $content = $this->encodingHelper->change($content, $charset, 'UTF-8');
1499 }
1500
1501 return $content;
1502 }
1503
1504 //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.
1505 return substr($content, 0, 100000);
1506 }
1507
1508 private function getMailParam(object $params): string
1509 {
1510 $searchIn = [];
1511
1512 if ($params->ifparameters) {
1513 $searchIn = array_merge($searchIn, $params->parameters);
1514 }
1515 if ($params->ifdparameters) {
1516 $searchIn = array_merge($searchIn, $params->dparameters);
1517 }
1518
1519 if (empty($searchIn)) {
1520 return '';
1521 }
1522
1523 foreach ($searchIn as $values) {
1524 if (strtolower($values->attribute) === 'charset') {
1525 return $values->value;
1526 }
1527 }
1528
1529 return '';
1530 }
1531
1532 public function getErrors(): array
1533 {
1534 $return = [];
1535 if (!$this->usePear) {
1536 if (!function_exists('imap_alerts')) {
1537 $return[] = 'The IMAP extension could not be loaded, please change your PHP configuration to enable it or use the pop3 method without imap extension';
1538
1539 return $return;
1540 }
1541 $alerts = $this->callImapFunction('imap_alerts');
1542 $errors = $this->callImapFunction('imap_errors');
1543 if (!empty($alerts)) {
1544 $return = array_merge($return, $alerts);
1545 }
1546 if (!empty($errors)) {
1547 $return = array_merge($return, $errors);
1548 }
1549 }
1550
1551 return $return;
1552 }
1553
1554 private function refreshToken(): void
1555 {
1556 if ($this->config->get('bounce_server') === 'imap.gmail.com') {
1557 $url = 'https://oauth2.googleapis.com/token';
1558 } else {
1559 $tenant = $this->config->get('bounce_tenant', 'consumers');
1560 $url = 'https://login.microsoftonline.com/'.$tenant.'/oauth2/v2.0/token';
1561 }
1562
1563 $oauth = new OAuth(
1564 [
1565 'sendingMethod' => 'bounce',
1566 'tokenGenerationUrl' => $url,
1567 'userName' => trim($this->config->get('bounce_username')),
1568 'clientId' => trim($this->config->get('bounce_client_id')),
1569 'clientSecret' => trim($this->config->get('bounce_client_secret')),
1570 'oauthToken' => trim($this->config->get('bounce_access_token')),
1571 'oauthTokenExpiration' => trim($this->config->get('bounce_access_token_expiration')),
1572 'refreshToken' => trim($this->config->get('bounce_refresh_token')),
1573 ]
1574 );
1575
1576 $this->oAuthToken = $oauth->getToken();
1577 }
1578
1579 private function callImapFunction(string $functionName, array $params = [])
1580 {
1581 if (in_array($this->server, self::HOSTS_NEEDING_OAUTH)) {
1582 $functionName = str_replace('imap_', 'imap2_', $functionName);
1583 }
1584
1585 if (empty($params)) {
1586 return $functionName();
1587 }
1588
1589 return $functionName(...$params);
1590 }
1591 }
1592