PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 10.1.4
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v10.1.4
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
acymailing / back / Helpers / BounceHelper.php

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

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