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

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