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