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 +1595 -0 11.1.1 → 11.1.0 View file →
@@ -1,0 +1,1595 @@
1 +<?php
2 +
3 +namespace AcyMailing\Helpers;
4 +
5 +use AcyMailerPhp\OAuth;
6 +use AcyMailing\Classes\HistoryClass;
7 +use AcyMailing\Classes\ListClass;
8 +use AcyMailing\Classes\MailClass;
9 +use AcyMailing\Classes\RuleClass;
10 +use AcyMailing\Classes\UserClass;
11 +use AcyMailing\Core\AcymObject;
12 +use AcyMailing\Libraries\Pear\Pop3;
13 +use AcyMailing\Types\CharsetType;
14 +
15 +class BounceHelper extends AcymObject
16 +{
17 + const HOSTS_NEEDING_OAUTH = [
18 + 'imap.gmail.com',
19 + 'outlook.office365.com',
20 + ];
21 +
22 + const MESSAGE_TYPE_SUCCESS = 0;
23 + const MESSAGE_TYPE_INFO = 1;
24 + const MESSAGE_TYPE_ERROR = 2;
25 +
26 + // Needed information for the connection
27 + private string $server;
28 + private string $username;
29 + private string $password;
30 + private string $port;
31 + private string $connectMethod;
32 + private string $secureMethod;
33 + private bool $selfSigned;
34 + private int $timeout;
35 + private string $oAuthToken;
36 +
37 + // Allowed extensions for uploaded files (attachments)
38 + private array $allowed_extensions = [];
39 + protected int $nbMessages = 0;
40 + public bool $report = false;
41 + private MailerHelper $mailer;
42 + private $mailbox;
43 + public $_message;
44 + private UserClass $userClass;
45 + private array $blockedUsers = [];
46 + private array $deletedUsers = [];
47 + private array $bounceMessages = [];
48 + private bool $usePear = false;
49 + private string $detectEmail;
50 + private string $detectEmail2 = '/(([a-z0-9\-]+\.)+[a-z0-9]{2,8})\/([a-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+)*)/i';
51 + public array $messages = [];
52 + //Max execution time minus 4 seconds... we need to stop the process before this date.
53 + public int $stoptime = 0;
54 + //Check apache module "mod_security" to avoid flush issue
55 + protected bool $mod_security2 = false;
56 + //Number of ob_end_flush used in the process...
57 + public int $obend = 0;
58 + // Correspond to mailbox entity in database
59 + public object $action;
60 + public array $attachments = [];
61 +
62 + private array $inlineImages = [];
63 + private array $allCharsets;
64 + private bool $errorDuringActions = false;
65 +
66 + private RuleClass $ruleClass;
67 + private MailClass $mailClass;
68 + private HistoryClass $historyClass;
69 + private EncodingHelper $encodingHelper;
70 +
71 + private array $allLists;
72 +
73 + public function __construct()
74 + {
75 + parent::__construct();
76 +
77 + $this->mailer = new MailerHelper();
78 + $this->ruleClass = new RuleClass();
79 + $this->userClass = new UserClass();
80 + $this->mailClass = new MailClass();
81 + $this->historyClass = new HistoryClass();
82 + $this->encodingHelper = new EncodingHelper();
83 +
84 + $this->mailer->report = false;
85 + $this->mailer->alreadyCheckedAddresses = true;
86 +
87 + $charsetType = new CharsetType();
88 + $this->allCharsets = $charsetType->charsets;
89 +
90 + $this->allowed_extensions = explode(',', $this->config->get('allowed_files', ''));
91 + $this->detectEmail = '/'.acym_getEmailRegex(false, true).'/i';
92 + }
93 +
94 + public function init(array $config = []): bool
95 + {
96 + if (empty($config)) {
97 + $config = [
98 + 'server' => $this->config->get('bounce_server'),
99 + 'username' => $this->config->get('bounce_username'),
100 + 'password' => $this->config->get('bounce_password'),
101 + 'port' => $this->config->get('bounce_port'),
102 + 'connect_method' => $this->config->get('bounce_connection'),
103 + 'secure_method' => $this->config->get('bounce_secured'),
104 + 'self_signed' => $this->config->get('bounce_certif', false),
105 + 'timeout' => $this->config->get('bounce_timeout', 10),
106 + 'bounce_access_token' => $this->config->get('bounce_access_token'),
107 + ];
108 + }
109 +
110 + $this->server = $config['server'];
111 + $this->username = $config['username'];
112 + $this->password = $config['password'];
113 + $this->port = $config['port'];
114 + $this->connectMethod = $config['connect_method'];
115 + $this->secureMethod = $config['secure_method'];
116 + $this->selfSigned = $config['self_signed'];
117 + $this->timeout = intval($config['timeout']);
118 + $this->oAuthToken = $config['bounce_access_token'];
119 +
120 + if ($this->connectMethod === 'pear') {
121 + $this->usePear = true;
122 + include_once ACYM_LIBRARIES.'Pear'.DS.'Pop3.php';
123 +
124 + return true;
125 + }
126 +
127 + require_once ACYM_LIBRARIES.'Imap2'.DS.'bootstrap.php';
128 +
129 + if (extension_loaded('imap') || function_exists('imap_open')) {
130 + return true;
131 + }
132 +
133 + if ($this->report) {
134 + $prefix = PHP_SHLIB_SUFFIX === 'dll' ? 'php_' : '';
135 + $extension = $prefix.'imap.'.PHP_SHLIB_SUFFIX;
136 +
137 + acym_display(
138 + 'The extension "'.$extension.'" could not be loaded, please change your PHP configuration to enable it or use the pop3 method without imap extension',
139 + 'error'
140 + );
141 + }
142 +
143 + return false;
144 + }
145 +
146 + public function connect(): bool
147 + {
148 + if ($this->usePear) {
149 + return $this->connectPear();
150 + }
151 +
152 + return $this->connectImap();
153 + }
154 +
155 + private function connectPear(): bool
156 + {
157 + ob_start();
158 +
159 + $this->mailbox = new Pop3();
160 +
161 + $timeout = $this->timeout;
162 + if (!empty($timeout)) {
163 + $this->mailbox->_timeout = $timeout;
164 + }
165 +
166 + $port = intval($this->port);
167 + $secure = $this->secureMethod;
168 + if (empty($port)) {
169 + if ($secure === 'ssl') {
170 + $port = '995';
171 + } else {
172 + $port = '110/pop3/notls';
173 + }
174 + }
175 +
176 + $serverName = trim($this->server);
177 +
178 + //We don't add back the ssl:// or tls:// if it's already there
179 + if (!empty($secure) && !strpos($serverName, '://')) {
180 + $serverName = $secure.'://'.$serverName;
181 + }
182 +
183 + if (!$this->mailbox->connect($serverName, $port)) {
184 + $warnings = ob_get_clean();
185 + if ($this->report) {
186 + $notification = [
187 + 'name' => 'error_connecting',
188 + 'removable' => 1,
189 + ];
190 + acym_enqueueMessage(acym_translationSprintf('ACYM_ERROR_CONNECTING', $this->server.' : '.$port), 'error', true, [$notification]);
191 + }
192 + if (!empty($warnings) && $this->report) {
193 + acym_display($warnings, 'warning');
194 + }
195 +
196 + return false;
197 + }
198 +
199 + $login = $this->mailbox->login(trim($this->username), trim($this->password), 'USER');
200 + if (empty($login) || isset($login->code)) {
201 + $warnings = ob_get_clean();
202 + if ($this->report) {
203 + acym_enqueueMessage(acym_translationSprintf('ACYM_ERROR_LOGIN', $this->username), 'error');
204 + }
205 + if (!empty($warnings) && $this->report) {
206 + acym_display($warnings, 'warning');
207 + }
208 +
209 + return false;
210 + }
211 +
212 + ob_end_clean();
213 +
214 + return true;
215 + }
216 +
217 + private function connectImap(): bool
218 + {
219 + if (empty($this->server)) {
220 + acym_enqueueMessage(acym_translation('ACYM_CONFIGURE_BOUNCE'), 'warning');
221 +
222 + return false;
223 + }
224 +
225 + ob_start();
226 + //First we reset the buffer or errors and warnings
227 + $buff = $this->callImapFunction('imap_alerts', []);
228 + $buff = $this->callImapFunction('imap_errors', []);
229 +
230 + $timeout = intval($this->timeout);
231 + if (!empty($timeout)) {
232 + $this->callImapFunction('imap_timeout', [IMAP_OPENTIMEOUT, $timeout]);
233 + }
234 +
235 + $port = intval($this->port);
236 + $secure = $this->secureMethod;
237 + $protocol = $this->connectMethod;
238 + $serverName = '{'.trim($this->server);
239 + if (empty($port)) {
240 + if ($protocol === 'imap') {
241 + $port = $secure === 'ssl' ? '993' : '143';
242 + } elseif ($protocol === 'pop3') {
243 + $port = $secure === 'ssl' ? '995' : '110';
244 + }
245 + }
246 +
247 + if (!empty($port)) {
248 + $serverName .= ':'.$port;
249 + }
250 +
251 + if ($this->server === 'outlook.office365.com') {
252 + $serverName .= '/imap';
253 + }
254 +
255 + //Add the secure protocol (TLS or SSL)
256 + if (!empty($secure)) {
257 + $serverName .= '/'.$secure;
258 + }
259 +
260 + // Test if string contains '\'
261 + $email = trim($this->username);
262 + if (strpos($email, '\\') !== false) {
263 + [$user, $authuser] = explode('\\', $email);
264 + [$x, $domain] = explode('@', $user);
265 + $serverName .= '/authuser='.$user.'/user='.$authuser.'@'.$domain;
266 + }
267 +
268 + if ($this->selfSigned) {
269 + $serverName .= '/novalidate-cert';
270 + }
271 +
272 + //Add the method (imap by default) ex : pop3
273 + if (!empty($protocol) && $this->server !== 'outlook.office365.com') {
274 + $serverName .= '/service='.$protocol;
275 + }
276 + $serverName .= '}';
277 + if (in_array($this->server, self::HOSTS_NEEDING_OAUTH)) {
278 + $this->refreshToken();
279 + if (empty($this->oAuthToken)) {
280 + return false;
281 + }
282 + $this->mailbox = imap2_open($serverName, trim($this->username), $this->oAuthToken, OP_XOAUTH2);
283 + } else {
284 + $this->mailbox = imap_open($serverName, trim($this->username), trim($this->password), OP_SILENT);
285 + }
286 + $warnings = ob_get_clean();
287 +
288 + if ($this->report) {
289 + if (!$this->mailbox) {
290 + $notification = [
291 + 'name' => 'error_connecting',
292 + 'removable' => 1,
293 + ];
294 + acym_enqueueMessage(acym_translationSprintf('ACYM_ERROR_CONNECTING', $serverName), 'error', true, [$notification]);
295 + }
296 + if (!empty($warnings)) {
297 + acym_enqueueMessage($warnings, 'warning');
298 + }
299 + }
300 +
301 + return (bool)$this->mailbox;
302 + }
303 +
304 + public function getNBMessages(): int
305 + {
306 + if ($this->usePear) {
307 + $this->nbMessages = intval($this->mailbox->numMsg());
308 + } else {
309 + $this->nbMessages = intval($this->callImapFunction('imap_num_msg', [$this->mailbox]));
310 + }
311 +
312 + return $this->nbMessages;
313 + }
314 +
315 + protected function getMessage(int $msgNB)
316 + {
317 + if ($this->usePear) {
318 + $message = new \stdClass();
319 + $message->headerString = $this->mailbox->getRawHeaders($msgNB);
320 + if (empty($message->headerString)) {
321 + return false;
322 + }
323 + } else {
324 + $message = $this->callImapFunction('imap_headerinfo', [$this->mailbox, $msgNB]);
325 + }
326 +
327 + return $message;
328 + }
329 +
330 + protected function deleteMessage($msgNB): void
331 + {
332 + if ($this->usePear) {
333 + $this->mailbox->deleteMsg($msgNB);
334 + } else {
335 + $this->callImapFunction('imap_delete', [$this->mailbox, $msgNB]);
336 + $this->callImapFunction('imap_expunge', [$this->mailbox]);
337 + }
338 + }
339 +
340 + public function close(): void
341 + {
342 + if ($this->usePear) {
343 + $this->mailbox->disconnect();
344 + } else {
345 + $this->callImapFunction('imap_close', [$this->mailbox]);
346 + }
347 + }
348 +
349 + public function decodeMessage(bool $attachments = false): bool
350 + {
351 + if ($this->usePear) {
352 + return $this->decodeMessagePear($attachments);
353 + } else {
354 + return $this->decodeMessageImap($attachments);
355 + }
356 + }
357 +
358 + private function decodeMessagePear(bool $attachments): bool
359 + {
360 + $this->_message->headerinfo = $this->mailbox->getParsedHeaders($this->_message->messageNB);
361 + if (empty($this->_message->headerinfo['subject'])) {
362 + if (empty($this->_message->headerinfo['Subject'])) {
363 + return false;
364 + }
365 + $this->_message->headerinfo['subject'] = $this->_message->headerinfo['Subject'];
366 + }
367 + $this->_message->text = '';
368 + $this->_message->html = $this->mailbox->getBody($this->_message->messageNB);
369 +
370 + if (!empty($this->_message->headerinfo['content-type']) && strpos($this->_message->headerinfo['content-type'], 'boundary') !== false) {
371 + $matches = [];
372 + preg_match('#boundary="([^"]+)"#i', $this->_message->headerinfo['content-type'], $matches);
373 +
374 + if (!empty($matches[1]) && strpos($this->_message->html, $matches[1]) !== false) {
375 + $inlineImages = [];
376 +
377 + $segments = explode('--'.$matches[1], $this->_message->html);
378 + foreach ($segments as $segment) {
379 + // Find the segment containing the html and text version
380 + if (strpos($segment, 'Content-Type: text/plain') !== false) {
381 + // Check if there is another boundary
382 + $matches = [];
383 + preg_match('#boundary="([^"]+)"#i', $segment, $matches);
384 +
385 + if (!empty($matches[1])) {
386 + $parts = explode('--'.$matches[1], $segment);
387 +
388 + foreach ($parts as $onePart) {
389 + $content = trim(preg_replace('#(charset=".*?\r\n)|Content-(Type|ID|Disposition|Transfer-Encoding):.*?\r\n#is', "", $onePart));
390 +
391 + if (strpos($onePart, 'Content-Transfer-Encoding') !== false) {
392 + preg_match('#Content-Transfer-Encoding: (.+)#i', $onePart, $encoding);
393 + $encoding = trim($encoding[1]);
394 + // Someone has an issue when the $encoding is equal to 'Content-Type:' only
395 + if (!empty($encoding) && $encoding !== 'Content-Type:') {
396 + $content = mb_convert_encoding($content, 'UTF-8', $encoding);
397 + }
398 + }
399 +
400 + if (strpos($onePart, "Content-Type: text/plain") !== false) {
401 + $this->_message->text .= ' '.$content;
402 + }
403 + if (strpos($onePart, "Content-Type: text/html") !== false) {
404 + $this->_message->html .= ' '.$content;
405 + }
406 + }
407 + } else {
408 + $this->_message->html .= ' '.trim(preg_replace('#(charset=".*?\r\n)|Content-(Type|ID|Disposition|Transfer-Encoding):.*?\r\n#is', "", $segment));
409 + }
410 + } elseif (preg_match("#Content-Type: .*?/(png|jpg|jpeg|gif)#i", $segment) === 1) {
411 + preg_match('#name="([^"]+)"#i', $segment, $filename);
412 + if (empty($filename) || empty($filename[1])) {
413 + continue;
414 + }
415 + $name = trim($filename[1]);
416 + $filename = $name;
417 + $extensionPos = strrpos($filename, '.');
418 +
419 + if ($extensionPos === false) {
420 + continue;
421 + }
422 +
423 + $extension = substr($filename, $extensionPos + 1);
424 +
425 + 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
430 + $filename = preg_replace('#[^a-zA-Z0-9]#Uis', '_', substr($filename, 0, $extensionPos));
431 +
432 + // Get the upload folder and create it if needed
433 + $uploadFolder = str_replace(['/', '\\'], DS, acym_getFilesFolder());
434 + $pathToUpload = ACYM_ROOT.trim($uploadFolder, DS).DS;
435 +
436 + // Rename if needed
437 + if (file_exists($pathToUpload.$filename.'.'.$extension)) {
438 + $fileNumber = 1;
439 + while (file_exists($pathToUpload.$filename.'_('.$fileNumber.').'.$extension)) {
440 + $fileNumber++;
441 + }
442 + $filename = $filename.'_('.$fileNumber.')';
443 + }
444 +
445 + if ($attachments && preg_match("#Content-Disposition: attachment;#i", $segment) === 1) {
446 + $data = trim(substr($segment, strpos($segment, "\r\n\r\n")));
447 + if (strpos($segment, 'Content-Transfer-Encoding: base64') !== false) {
448 + $data = base64_decode($data);
449 + }
450 +
451 + try {
452 + if (acym_writeFile($pathToUpload.$filename.'.'.$extension, $data)) {
453 + $attachment = new \stdClass();
454 + $attachment->filename = str_replace(DS, '/', trim($uploadFolder, '\\/')).'/'.$filename.'.'.$extension;
455 + $attachment->size = filesize(ACYM_ROOT.$attachment->filename);
456 + $this->attachments[] = $attachment;
457 + }
458 + } catch (\Exception $e) {
459 + $completeFilename = $filename.'.'.$extension;
460 + $this->display(acym_translationSprintf('ACYM_ERROR_UPLOADING_ATTACHMENT_X_TO_X', $completeFilename, $e->getMessage()), self::MESSAGE_TYPE_ERROR);
461 + }
462 + } else {
463 + preg_match('#Content-ID: <([^>]+)>#i', $segment, $contentID);
464 + if (empty($contentID) || empty($contentID[1])) {
465 + continue;
466 + }
467 + $contentID = trim($contentID[1]);
468 +
469 + $data = trim(substr($segment, strpos($segment, "\r\n\r\n")));
470 + if (strpos($segment, 'Content-Transfer-Encoding: base64') !== false) {
471 + $data = base64_decode($data);
472 + }
473 +
474 +
475 + try {
476 + if (acym_writeFile($pathToUpload.$filename.'.'.$extension, $data)) {
477 + $inlineImages['cid:'.$contentID] = acym_rootURI().$uploadFolder.'/'.$filename.'.'.$extension;
478 + }
479 + } catch (\Exception $e) {
480 + $this->display(acym_translationSprintf('ACYM_ERROR_UPLOAD_ATTACHMENT', $filename.'.'.$extension, $e->getMessage()), self::MESSAGE_TYPE_ERROR);
481 + }
482 + }
483 + }
484 + }
485 +
486 + if (!empty($inlineImages)) {
487 + $this->_message->html = str_replace(array_keys($inlineImages), $inlineImages, $this->_message->html);
488 + }
489 + }
490 + }
491 +
492 + $this->_message->subject = $this->decodeHeader($this->_message->headerinfo['subject']);
493 + if (empty($this->_message->header)) {
494 + $this->_message->header = new \stdClass();
495 + }
496 + $this->_message->header->sender_email = @$this->_message->headerinfo['return-path'];
497 + if (is_array($this->_message->header->sender_email)) {
498 + $this->_message->header->sender_email = reset($this->_message->header->sender_email);
499 + }
500 + if (!empty($this->_message->header->sender_email) && preg_match($this->detectEmail, $this->_message->header->sender_email, $results)) {
501 + $this->_message->header->sender_email = $results[0];
502 + }
503 + $this->_message->header->sender_name = acym_stripTags(@$this->_message->headerinfo['from'] ?? '');
504 + $this->_message->header->reply_to_email = $this->_message->header->sender_email;
505 + $this->_message->header->reply_to_name = $this->_message->header->sender_name;
506 + $this->_message->header->from_email = $this->_message->header->sender_email;
507 + $this->_message->header->from_name = $this->_message->header->sender_name;
508 +
509 + return true;
510 + }
511 +
512 + private function decodeMessageImap(bool $attachments): bool
513 + {
514 + $this->_message->structure = $this->callImapFunction('imap_fetchstructure', [$this->mailbox, $this->_message->messageNB]);
515 +
516 + if (empty($this->_message->structure)) {
517 + return false;
518 + }
519 +
520 + $this->_message->headerinfo = $this->callImapFunction('imap_fetchheader', [$this->mailbox, $this->_message->messageNB]);
521 +
522 + $this->_message->html = '';
523 + $this->_message->text = '';
524 +
525 + //Multipart message : type == 1
526 + if ($this->_message->structure->type == 1) {
527 + $this->_message->contentType = 2;
528 + if ($this->_message->structure->subtype === 'MIXED') {
529 + $allParts = $this->explodeBodyMixed($this->_message->structure);
530 + } else {
531 + $allParts = $this->explodeBody($this->_message->structure);
532 + }
533 +
534 + $text = '';
535 + foreach ($allParts as $num => $onePart) {
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') {
539 + $this->_message->html .= $decodedContent;
540 + } else {
541 + if ($onePart->subtype === 'PLAIN') {
542 + $text .= $decodedContent."\n";
543 + }
544 + $this->_message->text .= $decodedContent."\n\n- - -\n\n";
545 + }
546 +
547 + // Only for mailbox actions
548 + if (!empty($this->action) && $attachments) {
549 + if ((isset($onePart->parameters) && is_array($onePart->parameters)) || (isset($onePart->dparameters) && is_array($onePart->dparameters))) {
550 + $this->uploadAttachment($onePart, $num);
551 + } elseif (isset($onePart->parts) && is_array($onePart->parts)) {
552 + foreach ($onePart->parts as $num2 => $innerPart) {
553 + $this->uploadAttachment($innerPart, $num2);
554 + }
555 + }
556 + }
557 + }
558 +
559 + if (!empty($this->action) && empty($this->_message->html)) {
560 + $this->_message->html = $text."\n";
561 + }
562 + if (!empty($this->inlineImages)) {
563 + $this->_message->html = str_replace(array_keys($this->inlineImages), $this->inlineImages, $this->_message->html);
564 + }
565 + } else {
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') {
569 + $this->_message->contentType = 1;
570 + $this->_message->html = $decodedBody;
571 + } else {
572 + $this->_message->contentType = 0;
573 + $this->_message->text = $decodedBody;
574 + }
575 + }
576 +
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 + }
583 +
584 + $this->decodeAddressImap('sender');
585 + $this->decodeAddressImap('from');
586 + $this->decodeAddressImap('reply_to');
587 + $this->decodeAddressImap('to');
588 +
589 + return true;
590 + }
591 +
592 + private function uploadAttachment($attachment, $num): void
593 + {
594 + // It may be a real attachment, or an inline image
595 + if (!isset($attachment->disposition) || (strtolower($attachment->disposition) !== 'attachment' && strtolower($attachment->disposition) !== 'inline')) {
596 + return;
597 + }
598 +
599 + $name = empty($attachment->dparameters[0]->value) ? $attachment->parameters[0]->value : $attachment->dparameters[0]->value;
600 + $filename = $this->decodeHeader($name);
601 + $extensionPos = strrpos($filename, '.');
602 +
603 + if ($extensionPos === false) {
604 + foreach ($attachment->parameters as $key => $oneParam) {
605 + if (strtolower($oneParam->attribute) === 'name') {
606 + $filename = $this->decodeHeader($attachment->parameters[$key]->value);
607 + $extensionPos = strrpos($filename, '.');
608 + break;
609 + }
610 + }
611 + }
612 +
613 + // No extension => no upload
614 + if ($extensionPos === false) {
615 + return;
616 + }
617 +
618 + // Upload only allowed extensions in the configuration, and never executable ones
619 + $extension = substr($filename, $extensionPos + 1);
620 + if (!in_array(strtolower($extension), $this->allowed_extensions) || acym_isForbiddenFileExtension($filename)) {
621 + return;
622 + }
623 +
624 + // Make sure there is no double extension or space
625 + $filename = preg_replace('#[^a-zA-Z0-9]#Uis', '_', substr($filename, 0, $extensionPos));
626 +
627 + // Get the upload folder and create it if needed
628 + $uploadFolder = str_replace(['/', '\\'], DS, acym_getFilesFolder());
629 + $pathToUpload = ACYM_ROOT.trim($uploadFolder, DS).DS;
630 +
631 + // Rename if needed
632 + if (file_exists($pathToUpload.$filename.'.'.$extension)) {
633 + $fileNumber = 1;
634 + while (file_exists($pathToUpload.$filename.'_('.$fileNumber.').'.$extension)) {
635 + $fileNumber++;
636 + }
637 + $filename = $filename.'_('.$fileNumber.')';
638 + }
639 +
640 + $content = $this->callImapFunction('imap_fetchbody', [$this->mailbox, $this->_message->messageNB, $num]);
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 +
649 + try {
650 + if (acym_writeFile($pathToUpload.$filename.'.'.$extension, $data)) {
651 + if (
652 + strtolower($attachment->disposition) !== 'attachment'
653 + && !empty($attachment->id)
654 + && in_array(strtolower($attachment->subtype), acym_getImageFileExtensions())
655 + ) {
656 + $this->inlineImages['cid:'.trim($attachment->id, '<>')] = acym_rootURI().$uploadFolder.'/'.$filename.'.'.$extension;
657 + } else {
658 + $attachment = new \stdClass();
659 + $attachment->filename = str_replace(DS, '/', trim($uploadFolder, '\\/')).'/'.$filename.'.'.$extension;
660 + $attachment->size = filesize(ACYM_ROOT.$attachment->filename);
661 + $this->attachments[] = $attachment;
662 + }
663 + }
664 + } catch (\Exception $e) {
665 + $completeFilename = $filename.'.'.$extension;
666 + $this->display(acym_translationSprintf('ACYM_ERROR_UPLOADING_ATTACHMENT_X_TO_X', $completeFilename, $e->getMessage()), self::MESSAGE_TYPE_ERROR);
667 + }
668 + }
669 +
670 + public function handleMessages(): void
671 + {
672 + $maxMessages = min($this->nbMessages, $this->config->get('bounce_max', 0));
673 + //500 messages maximum at once
674 + if (empty($maxMessages)) {
675 + $maxMessages = min($this->nbMessages, 500);
676 + }
677 +
678 + if ($this->report) {
679 + if (function_exists('apache_get_modules')) {
680 + $modules = apache_get_modules();
681 + $this->mod_security2 = in_array('mod_security2', $modules);
682 + }
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.
686 + @ini_set('output_buffering', 'off');
687 + // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Disabled to prevent blank output pages.
688 + @ini_set('zlib.output_compression', 0);
689 +
690 +
691 + if (!headers_sent()) {
692 + while (ob_get_level() > 0 && $this->obend++ < 3) {
693 + @ob_end_flush();
694 + }
695 + }
696 +
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
708 + if (function_exists('ob_flush')) {
709 + @ob_flush();
710 + }
711 + if (!$this->mod_security2) {
712 + @flush();
713 + }
714 + }
715 +
716 + //We load all the published rules
717 + $rules = $this->ruleClass->getAll();
718 + $rules = array_filter($rules, function ($rule) {
719 + return $rule->active;
720 + });
721 +
722 + $msgNB = $maxMessages;
723 + $listClass = new ListClass();
724 + $this->allLists = $listClass->getAll('id');
725 +
726 + //Exclude some email addresses...
727 + $replyemail = $this->config->get('reply_email');
728 + $fromemail = $this->config->get('from_email');
729 + $bouncemail = $this->config->get('bounce_email');
730 + $removeEmails = '#('.str_replace(['%'], ['@'], $this->config->get('bounce_username'));
731 + if (!empty($bouncemail)) {
732 + $removeEmails .= '|'.$bouncemail;
733 + }
734 + if (!empty($fromemail)) {
735 + $removeEmails .= '|'.$fromemail;
736 + }
737 + if (!empty($replyemail)) {
738 + $removeEmails .= '|'.$replyemail;
739 + }
740 + $removeEmails .= ')#i';
741 +
742 + while (($msgNB > 0) && ($this->_message = $this->getMessage($msgNB))) {
743 + if ($this->report) {
744 + echo '<script type="text/javascript" language="javascript">setCounter('.esc_html($maxMessages - $msgNB + 1).')</script>';
745 + if (function_exists('ob_flush')) {
746 + @ob_flush();
747 + }
748 + if (!$this->mod_security2) {
749 + @flush();
750 + }
751 + }
752 + $this->_message->messageNB = $msgNB;
753 + $msgNB--;
754 +
755 + //We could not retrieve the message... we continue with the next message
756 + if (!$this->decodeMessage()) {
757 + $this->display(acym_translation('ACYM_ERROR_RETRIEVING_MESSAGE'), self::MESSAGE_TYPE_ERROR, $maxMessages - $this->_message->messageNB + 1);
758 + continue;
759 + }
760 +
761 + if (empty($this->_message->subject)) {
762 + $this->_message->subject = '';
763 + }
764 +
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
767 + if (!empty($this->_message->header->from_email)) {
768 + $this->_message->analyseText .= ' '.$this->_message->header->from_email;
769 + }
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 + );
775 +
776 + //Identify the user and the e-mail...
777 + preg_match('#AC([0-9]+)Y([0-9]+)BA#i', $this->_message->analyseText, $resultsVars);
778 + if (!empty($resultsVars[1])) {
779 + $this->_message->userid = $resultsVars[1];
780 + }
781 + if (!empty($resultsVars[2])) {
782 + $this->_message->mailid = $resultsVars[2];
783 + }
784 +
785 + if (empty($this->_message->userid)) {
786 + //We will need the e-mail itself in that case... :p
787 + preg_match_all($this->detectEmail, $this->_message->analyseText, $results);
788 +
789 + //Still no result? We try to find others
790 + if (empty($results[0])) {
791 + preg_match_all($this->detectEmail2, $this->_message->analyseText, $results2);
792 + for ($i = 0; $i < count($results2[0]); $i++) {
793 + $results[0][] = $results2[3][$i].'@'.$results2[1][$i];
794 + }
795 + }
796 +
797 + if (!empty($results[0])) {
798 + $alreadyChecked = [];
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
801 + if (!preg_match($removeEmails, $oneEmail)) {
802 + //We will keep this one, so we make sure it's strtolower
803 + $this->_message->subemail = strtolower($oneEmail);
804 + //We already checked this e-mail address... no need to try it a second time
805 + if (!empty($alreadyChecked[$this->_message->subemail])) {
806 + continue;
807 + }
808 +
809 + $user = $this->userClass->getOneByEmail($this->_message->subemail);
810 + if (!empty($user)) $this->_message->userid = $user->id;
811 +
812 + if (empty($this->_message->userid) && preg_match('#(&|\?)id=([0-9]+)&#ims', $this->_message->subemail, $subid)) {
813 + $user = $this->userClass->getOneById($subid[2]);
814 + $this->_message->userid = $user->id;
815 + $this->_message->subemail = $user->email;
816 + }
817 +
818 + $alreadyChecked[$this->_message->subemail] = true;
819 + if (!empty($this->_message->userid)) {
820 + break;
821 + }
822 + }
823 + }
824 + }
825 + }
826 +
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!!
829 + $this->_message->mailid = acym_loadResult(
830 + 'SELECT `mail_id` FROM #__acym_user_stat WHERE `user_id` = '.intval($this->_message->userid).' ORDER BY `send_date` DESC'
831 + );
832 + }
833 +
834 + foreach ($rules as $oneRule) {
835 + //We stop as soon as we find a good rule...
836 + if ($this->handleRule($oneRule)) {
837 + break;
838 + }
839 + }
840 +
841 + if ($msgNB % 50 == 0) {
842 + $this->userActions();
843 + }
844 +
845 + //We don't have time to finish the process? Ok, we stop it now!
846 + if (!empty($this->stoptime) && time() > $this->stoptime) {
847 + break;
848 + }
849 + }
850 +
851 + $this->userActions();
852 + $this->close();
853 + }
854 +
855 + /**
856 + * Execute actions on the subscriber... and record the statistics
857 + * We group them to not have performance issues
858 + */
859 + private function userActions(): void
860 + {
861 + if (!empty($this->deletedUsers)) {
862 + acym_arrayToInteger($this->deletedUsers);
863 + $this->userClass->delete($this->deletedUsers);
864 + $this->deletedUsers = [];
865 + }
866 +
867 + if (!empty($this->blockedUsers)) {
868 + acym_arrayToInteger($this->blockedUsers);
869 + $this->userClass->deactivate($this->blockedUsers);
870 + //We delete any other e-mail from the queue as well
871 + $allUsersId = implode(',', $this->blockedUsers);
872 + acym_query('DELETE FROM `#__acym_queue` WHERE `user_id` IN ('.$allUsersId.')');
873 + $this->blockedUsers = [];
874 + }
875 +
876 + if (!empty($this->bounceMessages)) {
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
881 +
882 + $updateBounceDetails = '';
883 + if (!empty($bouncedata['bouncedetails'])) {
884 + $bouncedetails = acym_loadResult('SELECT `bounce_details` FROM #__acym_mail_stat WHERE mail_id = '.intval($mailid));
885 + if (!empty($bouncedetails)) {
886 + $bouncedetails = unserialize($bouncedetails);
887 + } else {
888 + $bouncedetails = [];
889 + }
890 +
891 + foreach ($bouncedata['bouncedetails'] as $ruleName => $nbTimes) {
892 + if (empty($bouncedetails[$ruleName])) {
893 + $bouncedetails[$ruleName] = 0;
894 + }
895 + $bouncedetails[$ruleName] += $nbTimes;
896 + }
897 +
898 + $updateBounceDetails = ' , `bounce_details` = '.acym_escapeDB(serialize($bouncedetails));
899 + }
900 +
901 + if (!empty($bouncedata['userids'])) {
902 + $valueInsert = [];
903 + foreach ($bouncedata['ruletriggered'] as $userid => $rulename) {
904 + $valueInsert[] = '('.intval($mailid).','.intval($userid).',1,'.acym_escapeDB($rulename).')';
905 + }
906 + acym_query(
907 + 'INSERT INTO #__acym_user_stat (mail_id,user_id,bounce,bounce_rule) VALUES '.implode(
908 + ',',
909 + $valueInsert
910 + ).' ON DUPLICATE KEY UPDATE `bounce` = `bounce` + 1, bounce_rule=VALUES(bounce_rule)'
911 + );
912 + //We updated some profiles... let's make sure we really handle only unique bounces then and don't count it twice
913 + acym_arrayToInteger($bouncedata['userids']);
914 + $realUniqueBounces = acym_loadResult(
915 + 'SELECT COUNT(*)
916 + FROM #__acym_user_stat
917 + WHERE `bounce` = 1
918 + AND `user_id` IN ('.implode(',', $bouncedata['userids']).')
919 + AND `mail_id` = '.intval($mailid)
920 + );
921 + $bouncedata['nbbounces'] = $bouncedata['nbbounces'] - count($bouncedata['userids']) + $realUniqueBounces;
922 + }
923 +
924 + acym_query(
925 + 'UPDATE #__acym_mail_stat SET `bounce_unique` = `bounce_unique` + '.intval($bouncedata['nbbounces']).$updateBounceDetails.'
926 + WHERE `mail_id` = '.intval($mailid).'
927 + LIMIT 1'
928 + );
929 + }
930 + $this->bounceMessages = [];
931 + }
932 + }
933 +
934 + private function handleRule(object &$oneRule): bool
935 + {
936 + $regex = $oneRule->regex;
937 + if (empty($regex)) {
938 + return false;
939 + }
940 +
941 + //Do it based on the config of the rule...
942 +
943 + $analyseText = '';
944 + if (in_array('senderInfo', $oneRule->executed_on)) {
945 + $analyseText .= ' ';
946 + if (isset($this->_message->header->sender_name)) {
947 + $analyseText .= $this->_message->header->sender_name;
948 + }
949 + if (isset($this->_message->header->sender_email)) {
950 + $analyseText .= $this->_message->header->sender_email;
951 + }
952 + }
953 +
954 + if (in_array('subject', $oneRule->executed_on)) {
955 + $analyseText .= ' '.$this->_message->subject;
956 + }
957 +
958 + if (in_array('body', $oneRule->executed_on)) {
959 + if (!empty($this->_message->html)) {
960 + $analyseText .= ' '.$this->_message->html;
961 + }
962 + if (!empty($this->_message->text)) {
963 + $analyseText .= ' '.$this->_message->text;
964 + }
965 + }
966 +
967 + //Because it's easier to handle it that way... for multilines.
968 + $analyseText = str_replace(["\n", "\r", "\t"], ' ', $analyseText);
969 +
970 + if (!preg_match('#'.$regex.'#ims', $analyseText)) {
971 + return false;
972 + }
973 +
974 + $message = acym_translation('ACYM_BOUNCE_RULE').' ['.acym_translation('ACYM_ID').' '.$oneRule->id.'] '.acym_translation($oneRule->name).' : ';
975 + $this->errorDuringActions = false;
976 + $message .= $this->actionUser($oneRule);
977 + $message .= $this->actionMessage($oneRule);
978 +
979 + $type = self::MESSAGE_TYPE_SUCCESS;
980 + if ($this->errorDuringActions) {
981 + $type = self::MESSAGE_TYPE_ERROR;
982 + }
983 +
984 + $this->display($message, $type);
985 +
986 + return true;
987 + }
988 +
989 + private function actionUser(object &$oneRule): string
990 + {
991 + $commonListsMailUser = [];
992 + $message = '';
993 +
994 + if (empty($this->_message->userid)) {
995 + $message .= 'user not identified';
996 + if (!empty($this->_message->subemail)) {
997 + $message .= ' ( '.$this->_message->subemail.' ) ';
998 + }
999 +
1000 + return $message;
1001 + }
1002 +
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 + ) {
1009 + if (empty($this->_message->subemail)) {
1010 + $currentUser = $this->userClass->getOneById($this->_message->userid);
1011 + if (!empty($currentUser->email)) {
1012 + $this->_message->subemail = $currentUser->email;
1013 + }
1014 + }
1015 +
1016 + $userSubscription = $this->userClass->getSubscriptionStatus($this->_message->userid);
1017 +
1018 + $bounceContent = '';
1019 + if (!empty($this->_message->html)) {
1020 + $bounceContent .= ' '.$this->_message->html;
1021 + }
1022 + if (!empty($this->_message->text)) {
1023 + $bounceContent .= ' '.$this->_message->text;
1024 + }
1025 +
1026 + $emailLists = [];
1027 + preg_match('#Please unsubscribe user ID \d+ from list\(s\) ([^.]+)\.#Uis', $bounceContent, $emailLists);
1028 + if (!empty($emailLists[1])) {
1029 + $currentEmailLists = explode(',', $emailLists[1]);
1030 + foreach ($currentEmailLists as $oneListId) {
1031 + if (!empty($userSubscription[$oneListId]) && $userSubscription[$oneListId]->status == 1) {
1032 + $commonListsMailUser[] = $oneListId;
1033 + }
1034 + }
1035 + }
1036 +
1037 + if (empty($commonListsMailUser)) {
1038 + $commonListsMailUser = array_keys($userSubscription);
1039 + }
1040 + }
1041 +
1042 + if (empty($this->_message->subemail)) {
1043 + $this->_message->subemail = $this->_message->userid;
1044 + }
1045 +
1046 +
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 +
1054 + if ($oneRule->increment_stats && !empty($this->_message->mailid) && !empty($mail)) {
1055 + //Init the stats...
1056 + if (empty($this->bounceMessages[$this->_message->mailid])) {
1057 + $this->bounceMessages[$this->_message->mailid] = [];
1058 + $this->bounceMessages[$this->_message->mailid]['nbbounces'] = 0;
1059 + $this->bounceMessages[$this->_message->mailid]['userids'] = [];
1060 + $this->bounceMessages[$this->_message->mailid]['bouncedetails'] = [];
1061 + $this->bounceMessages[$this->_message->mailid]['ruletriggered'] = [];
1062 + }
1063 +
1064 + //Increment the global stats...
1065 + $this->bounceMessages[$this->_message->mailid]['nbbounces']++;
1066 +
1067 + //Increment the detailed stats...
1068 + $ruleName = $oneRule->name.' [ID '.$oneRule->id.'] ';
1069 + //We add a @ just in case the rule was not already defined...
1070 + if (empty($this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName])) {
1071 + $this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName] = 1;
1072 + } else {
1073 + $this->bounceMessages[$this->_message->mailid]['bouncedetails'][$ruleName]++;
1074 + }
1075 +
1076 + $user = $this->userClass->getOneById($this->_message->userid);
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
1079 + $this->bounceMessages[$this->_message->mailid]['userids'][] = intval($this->_message->userid);
1080 + $this->bounceMessages[$this->_message->mailid]['ruletriggered'][intval($this->_message->userid)] = $oneRule->name.' ['.acym_translation(
1081 + 'ACYM_ID'
1082 + ).' '.$oneRule->id.']';
1083 + }
1084 + }
1085 +
1086 + //Make sure we have enough messages to really execute this
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...
1089 + if (empty($this->_message->mailid)) {
1090 + $this->_message->mailid = 0;
1091 + }
1092 + $nb = intval(
1093 + acym_loadResult(
1094 + 'SELECT COUNT(mail_id) FROM #__acym_user_stat WHERE bounce > 0 AND user_id = '.intval($this->_message->userid).' AND mail_id != '.intval(
1095 + $this->_message->mailid
1096 + )
1097 + )
1098 + ) + 1;
1099 +
1100 + if ($nb < $oneRule->execute_action_after) {
1101 + $message .= ' | '.acym_translationSprintf('ACYM_BOUNCE_RECEIVED', $nb, $this->_message->subemail).' | '.acym_translationSprintf(
1102 + 'ACYM_BOUNCE_MIN_EXEC',
1103 + $oneRule->execute_action_after
1104 + );
1105 +
1106 + return $message;
1107 + }
1108 + }
1109 +
1110 + //If we delete the subscriber, it's the last action we execute
1111 + if (in_array('delete_user', $oneRule->action_user)) {
1112 + $message .= ' | '.acym_translationSprintf('ACYM_USER_X_DELETED', $this->_message->subemail);
1113 + $this->deletedUsers[] = intval($this->_message->userid);
1114 +
1115 + return $message;
1116 + }
1117 +
1118 + //We will need a default listid after...
1119 + $listId = 0;
1120 + if (in_array('subscribe_user', $oneRule->action_user) && !empty($oneRule->action_user['subscribe_user_list'])) {
1121 + $listId = $oneRule->action_user['subscribe_user_list'];
1122 +
1123 + $listName = empty($this->allLists[$listId]->name) ? $listId : $this->allLists[$listId]->name;
1124 + $message .= ' | ';
1125 + if (isset($userSubscription[$listId])) {
1126 + $message .= acym_translationSprintf('ACYM_USER_X_NOT_SUBSCRIBED_TO', $this->_message->subemail, $listName);
1127 + if ($userSubscription[$listId]->status == 1) {
1128 + $message .= acym_translation('ACYM_USER_ALREADY_SUBSCRIBED');
1129 + } elseif ($userSubscription[$listId]->status == 0) {
1130 + $message .= acym_translation('ACYM_USER_ALREADY_UNSUBSCRIBED');
1131 + }
1132 + } else {
1133 + $this->userClass->subscribe([$this->_message->userid], [$listId]);
1134 + $message .= acym_translationSprintf('ACYM_USER_X_SUBSCRIBED_TO', $this->_message->subemail, $listName);
1135 + }
1136 + }
1137 +
1138 + if (in_array('delete_user_subscription', $oneRule->action_user)) {
1139 + $removeLists = array_diff($commonListsMailUser, [$listId]);
1140 + if (!empty($removeLists)) {
1141 + $listNames = [];
1142 + foreach ($removeLists as $oneListId) {
1143 + if (!empty($this->allLists[$oneListId]->name)) {
1144 + $listNames[] = $this->allLists[$oneListId]->name;
1145 + } else {
1146 + $listNames[] = $oneListId;
1147 + }
1148 + }
1149 + $message .= ' | '.acym_translationSprintf('ACYM_USER_X_REMOVED_FROM', $this->_message->subemail, implode(', ', $listNames));
1150 + $this->userClass->removeSubscription([$this->_message->userid], $removeLists);
1151 + } else {
1152 + $message .= ' | '.acym_translationSprintf('ACYM_USER_X_NOT_SUBSCRIBED', $this->_message->subemail);
1153 + }
1154 + }
1155 +
1156 + if (in_array('unsubscribe_user', $oneRule->action_user)) {
1157 + $unsubLists = array_diff($commonListsMailUser, [$listId]);
1158 + if (!empty($unsubLists)) {
1159 + $listNames = [];
1160 + foreach ($unsubLists as $oneListId) {
1161 + if (!empty($this->allLists[$oneListId]->name)) {
1162 + $listNames[] = $this->allLists[$oneListId]->name;
1163 + } else {
1164 + $listNames[] = $oneListId;
1165 + }
1166 + }
1167 + $this->userClass->unsubscribe([$this->_message->userid], $unsubLists);
1168 + $message .= ' | '.acym_translationSprintf('ACYM_USER_X_UNSUBSCRIBED_FROM', $this->_message->subemail, implode(', ', $listNames));
1169 + } else {
1170 + $message .= ' | '.acym_translationSprintf('ACYM_USER_X_NOT_SUBSCRIBED', $this->_message->subemail);
1171 + }
1172 + }
1173 +
1174 + if (in_array('block_user', $oneRule->action_user)) {
1175 + $message .= ' | '.acym_translationSprintf('ACYM_USER_X_BLOCKED', $this->_message->subemail.' ( '.acym_translation('ACYM_ID').' '.intval($this->_message->userid).' )');
1176 + $this->blockedUsers[] = intval($this->_message->userid);
1177 + }
1178 +
1179 + if (in_array('empty_queue_user', $oneRule->action_user)) {
1180 + $affected = acym_query('DELETE FROM #__acym_queue WHERE user_id = '.intval($this->_message->userid));
1181 + $message .= ' | '.acym_translationSprintf(
1182 + 'ACYM_USER_X_QUEUE',
1183 + $this->_message->subemail.' ( '.acym_translation('ACYM_ID').' '.intval($this->_message->userid).' )',
1184 + acym_translationSprintf('ACYM_SUCC_DELETE_ELEMENTS', $affected)
1185 + );
1186 + }
1187 +
1188 + return $message;
1189 + }
1190 +
1191 + private function actionMessage(object &$oneRule): string
1192 + {
1193 + $message = '';
1194 +
1195 + //Fix the rule if needed... when the forwarded user is the same as the bounce e-mail address...
1196 + if (!empty($oneRule->action_message['forward_to'])) {
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
1202 + $oneRule->action_message['forward_to'] = '';
1203 + //We don't delete it
1204 + unset($oneRule->action_message['delete_message']);
1205 + $message .= ' | '.acym_translation('ACYM_BOUNCE_NOT_FORWARD');
1206 + }
1207 + }
1208 +
1209 + //Handle actions on the message itself
1210 +
1211 + // We only delete the message if the forward didn't fail and the save in history succeeded
1212 + $donotdelete = false;
1213 +
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?
1216 + $data = [];
1217 + $data[] = 'SUBJECT::'.@htmlentities($this->_message->subject, ENT_COMPAT, 'UTF-8');
1218 + $data[] = 'ACY_RULE::'.$oneRule->id.' '.$oneRule->name;
1219 + $data[] = 'REPLYTO_ADDRESS::'.$this->_message->header->reply_to_name.' ( '.$this->_message->header->reply_to_email.' )';
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.
1222 + $data[] = @htmlentities(print_r($this->_message->headerinfo, true), ENT_COMPAT, 'UTF-8');
1223 + if (empty($this->_message->mailid)) {
1224 + $this->_message->mailid = 0;
1225 + }
1226 + try {
1227 + $this->historyClass->insert($this->_message->userid, 'bounce', $data, $this->_message->mailid);
1228 + $message .= ' | '.acym_translationSprintf('ACYM_BOUNCE_MESSAGE_SAVED', $this->_message->userid);
1229 + } catch (\Exception $e) {
1230 + $message .= ' | '.acym_translationSprintf(
1231 + 'ACYM_BOUNCE_SAVE_ERROR_USER_MAIL_RULE',
1232 + $this->_message->userid,
1233 + $this->_message->mailid,
1234 + $oneRule->id,
1235 + $e->getMessage()
1236 + );
1237 + $donotdelete = true;
1238 + }
1239 + }
1240 +
1241 +
1242 + //We don't forward the message if it's the same mailbox!
1243 + if (!empty($oneRule->action_message['forward_to'])) {
1244 + //Get the forward address :
1245 + $this->mailer->clearAll();
1246 + $this->mailer->Subject = 'BOUNCE FORWARD : '.$this->_message->subject;
1247 +
1248 + if (substr_count($oneRule->action_message['forward_to'], '@') > 1) {
1249 + $forwardAddresses = explode(';', str_replace([';', ','], ';', $oneRule->action_message['forward_to']));
1250 + } else {
1251 + $forwardAddresses = [$oneRule->action_message['forward_to']];
1252 + }
1253 +
1254 + foreach ($forwardAddresses as $oneForwardAddress) {
1255 + $this->mailer->AddAddress($this->mailer->cleanText($oneForwardAddress));
1256 + }
1257 +
1258 + //Add the rule at the top so we know why the message has been forwarded.
1259 + $info = acym_translation('ACYM_BOUNCE_RULE').' ['.acym_translation('ACYM_ID').' '.$oneRule->id.'] '.acym_translation($oneRule->name);
1260 + if (!empty($this->_message->html)) {
1261 + $this->mailer->isHTML();
1262 + if (!empty($this->_message->userid)) {
1263 + $info .= ' ('.acym_translation('ACYM_USER_ID').': '.$this->_message->userid.')';
1264 + } elseif (!empty($this->_message->subemail)) {
1265 + $info .= ' ('.acym_translation('ACYM_USER_EMAIL').': '.$this->_message->subemail.')';
1266 + }
1267 +
1268 + $this->mailer->Body = $info.'<br />'.$this->_message->html;
1269 + if (!empty($this->_message->text)) {
1270 + $this->mailer->Body .= '<br /><br />-------<br />'.nl2br($this->_message->text);
1271 + }
1272 + } else {
1273 + $this->mailer->isHTML(false);
1274 + $this->mailer->Body = $info."\n".$this->_message->text;
1275 + }
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.
1280 + $this->mailer->Body .= print_r($this->_message->headerinfo, true);
1281 + $replyAddress = trim(@$this->_message->header->reply_to_email, '<> ');
1282 + if (!empty($replyAddress)) {
1283 + if ($this->config->get('mailer_method') === \plgAcymAcymailer::SENDING_METHOD_ID) {
1284 + $this->mailer->AddReplyTo($this->config->get('replyto_email'), $this->config->get('replyto_name'));
1285 + } else {
1286 + $this->mailer->AddReplyTo(trim($this->_message->header->reply_to_email, '<> '), $this->_message->header->reply_to_name);
1287 + }
1288 + }
1289 +
1290 + $this->mailer->isForward = true;
1291 + if ($this->mailer->send()) {
1292 + $message .= ' | '.acym_translationSprintf('ACYM_FORWARDED_TO_X', $oneRule->action_message['forward_to']);
1293 + } else {
1294 + $message .= ' | '.acym_translationSprintf('ACYM_NOT_FORWARDED_TO_X', $oneRule->action_message['forward_to'], $this->mailer->reportMessage);
1295 + $this->errorDuringActions = true;
1296 + }
1297 + }
1298 +
1299 + if (in_array('delete_message', $oneRule->action_message) && !$this->errorDuringActions) {
1300 + $message .= ' | '.acym_translation('ACYM_MESSAGE_DELETED');
1301 + $this->deleteMessage($this->_message->messageNB);
1302 + }
1303 +
1304 + return $message;
1305 + }
1306 +
1307 + private function decodeAddressImap(string $type): void
1308 + {
1309 + $address = $type.'address';
1310 + $name = $type.'_name';
1311 + $email = $type.'_email';
1312 + if (empty($this->_message->$type)) {
1313 + return;
1314 + }
1315 +
1316 + $var = $this->_message->$type;
1317 +
1318 + if (empty($this->_message->header)) {
1319 + $this->_message->header = new \stdClass();
1320 + }
1321 +
1322 + if (!empty($this->_message->$address)) {
1323 + $this->_message->header->$name = $this->_message->$address;
1324 + } else {
1325 + $this->_message->header->$name = $var[0]->personal;
1326 + }
1327 +
1328 + $this->_message->header->$email = $var[0]->mailbox.'@'.@$var[0]->host;
1329 + }
1330 +
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
1335 + {
1336 + $this->messages[] = $message;
1337 +
1338 + if (!$this->report) return;
1339 +
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 +
1348 + if (!empty($num)) {
1349 + echo '<br />'.esc_html($num).' : ';
1350 + } else {
1351 + echo '<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
1352 + }
1353 +
1354 + echo '<span style="color: '.esc_attr($color).'">'.esc_html($message).'</span>';
1355 +
1356 + if (function_exists('ob_flush')) {
1357 + @ob_flush();
1358 + }
1359 + if (!$this->mod_security2) {
1360 + @flush();
1361 + }
1362 + }
1363 +
1364 + public function decodeHeader(string $input): string
1365 + {
1366 + // Remove white space between encoded-words
1367 + $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
1368 + $currentCharset = false;
1369 +
1370 + // For each encoded-word...
1371 + while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
1372 + $encoded = $matches[1];
1373 + $charset = $matches[2];
1374 + $encoding = $matches[3];
1375 + $text = $matches[4];
1376 +
1377 + switch (strtolower($encoding)) {
1378 + case 'b':
1379 + $text = base64_decode($text);
1380 + break;
1381 +
1382 + case 'q':
1383 + $text = str_replace('_', ' ', $text);
1384 + preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
1385 + foreach ($matches[1] as $value) {
1386 + $text = str_replace('='.$value, chr(hexdec($value)), $text);
1387 + }
1388 + break;
1389 + }
1390 + $currentCharset = $charset;
1391 + $input = str_replace($encoded, $text, $input);
1392 + }
1393 +
1394 + //If we have a charset and we can handle it...
1395 + if (!empty($currentCharset) && in_array($currentCharset, $this->allCharsets)) {
1396 + $input = $this->encodingHelper->change($input, $currentCharset, 'UTF-8');
1397 + }
1398 +
1399 + return $input;
1400 + }
1401 +
1402 + private function explodeBodyMixed(object $struct, string $path = '1'): array
1403 + {
1404 + $allParts = [];
1405 +
1406 + if (empty($struct->parts)) {
1407 + return $allParts;
1408 + }
1409 +
1410 + $pathPrefix = ($struct->subtype === 'MIXED' && $path === '1') ? '' : $path.'.';
1411 + foreach ($struct->parts as $i => $part) {
1412 + $partPath = $pathPrefix.($i + 1);
1413 + if ($part->type == 1) {
1414 + $allParts = array_merge($this->explodeBodyMixed($part, $partPath), $allParts);
1415 + } else {
1416 + $allParts[$partPath] = $part;
1417 + }
1418 + }
1419 +
1420 + return $allParts;
1421 + }
1422 +
1423 + private function explodeBody(object $struct, string $path = '0', bool $inline = false): array
1424 + {
1425 + $allParts = [];
1426 +
1427 + if (empty($struct->parts)) {
1428 + return $allParts;
1429 + }
1430 +
1431 + //counts real content
1432 + $c = 0;
1433 + foreach ($struct->parts as $part) {
1434 + if ($part->type == 1) {
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
1445 + $newpath = $this->incPath($path);
1446 + $path = $this->incPath($path);
1447 + $allParts = array_merge($this->explodeBody($part, $newpath, true), $allParts);
1448 + }
1449 + } else {
1450 + $c++;
1451 + //creating new tree if this is part of a alternativ or rfc message:
1452 + if ($c == 1 && $inline) {
1453 + $path = $path.'.0';
1454 + }
1455 + //saving content:
1456 + $path = $this->incPath($path);
1457 + //print "<br> Content ".$path."<br>"; //debug information
1458 + $allParts[$path] = $part;
1459 + }
1460 + }
1461 +
1462 + return $allParts;
1463 + }
1464 +
1465 + //Increases the Path to the parts:
1466 + private function incPath(string $path): string
1467 + {
1468 + $newPath = '';
1469 + $path_elements = explode('.', $path);
1470 + $limit = count($path_elements);
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;
1476 + } else {
1477 + //rebuild "1.2.2"-Chronology
1478 + $newPath .= $path_elements[$i].'.';
1479 + }
1480 + }
1481 +
1482 + return $newPath;
1483 + }
1484 +
1485 + private function decodeContent(string $content, object $structure): string
1486 + {
1487 + $encoding = $structure->encoding;
1488 +
1489 + //First we decode the content properly
1490 + if ($encoding == 2) {
1491 + $content = imap_binary($content);
1492 + } elseif ($encoding == 3) {
1493 + $content = base64_decode($content, true);
1494 + } elseif ($encoding == 4) {
1495 + $content = imap_qprint($content);
1496 + }
1497 +
1498 + // Now we convert into utf-8! only for distribution lists
1499 + if (!empty($this->action)) {
1500 + $charset = $this->getMailParam($structure);
1501 + if (!empty($charset) && strtoupper($charset) !== 'UTF-8') {
1502 + $content = $this->encodingHelper->change($content, $charset, 'UTF-8');
1503 + }
1504 +
1505 + return $content;
1506 + }
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.
1509 + return substr($content, 0, 100000);
1510 + }
1511 +
1512 + private function getMailParam(object $params): string
1513 + {
1514 + $searchIn = [];
1515 +
1516 + if ($params->ifparameters) {
1517 + $searchIn = array_merge($searchIn, $params->parameters);
1518 + }
1519 + if ($params->ifdparameters) {
1520 + $searchIn = array_merge($searchIn, $params->dparameters);
1521 + }
1522 +
1523 + if (empty($searchIn)) {
1524 + return '';
1525 + }
1526 +
1527 + foreach ($searchIn as $values) {
1528 + if (strtolower($values->attribute) === 'charset') {
1529 + return $values->value;
1530 + }
1531 + }
1532 +
1533 + return '';
1534 + }
1535 +
1536 + public function getErrors(): array
1537 + {
1538 + $return = [];
1539 + if (!$this->usePear) {
1540 + if (!function_exists('imap_alerts')) {
1541 + $return[] = 'The IMAP extension could not be loaded, please change your PHP configuration to enable it or use the pop3 method without imap extension';
1542 +
1543 + return $return;
1544 + }
1545 + $alerts = $this->callImapFunction('imap_alerts');
1546 + $errors = $this->callImapFunction('imap_errors');
1547 + if (!empty($alerts)) {
1548 + $return = array_merge($return, $alerts);
1549 + }
1550 + if (!empty($errors)) {
1551 + $return = array_merge($return, $errors);
1552 + }
1553 + }
1554 +
1555 + return $return;
1556 + }
1557 +
1558 + private function refreshToken(): void
1559 + {
1560 + if ($this->config->get('bounce_server') === 'imap.gmail.com') {
1561 + $url = 'https://oauth2.googleapis.com/token';
1562 + } else {
1563 + $tenant = $this->config->get('bounce_tenant', 'consumers');
1564 + $url = 'https://login.microsoftonline.com/'.$tenant.'/oauth2/v2.0/token';
1565 + }
1566 +
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 + ]
1578 + );
1579 +
1580 + $this->oAuthToken = $oauth->getToken();
1581 + }
1582 +
1583 + private function callImapFunction(string $functionName, array $params = [])
1584 + {
1585 + if (in_array($this->server, self::HOSTS_NEEDING_OAUTH)) {
1586 + $functionName = str_replace('imap_', 'imap2_', $functionName);
1587 + }
1588 +
1589 + if (empty($params)) {
1590 + return $functionName();
1591 + }
1592 +
1593 + return $functionName(...$params);
1594 + }
1595 +}