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

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