46 private const MESSAGE_PARTS_TEXT = 1;
47 private const MESSAGE_PARTS_ATTACHMENT = 2;
48 private const MESSAGE_PARTS_ALL = -1;
50 private const COMPARISON_ATTACHMENT_LEVEL_STRONG = 2;
51 private const COMPARISON_ATTACHMENT_LEVEL_AVERAGE = 1;
52 private const COMPARISON_ATTACHMENT_LEVEL_LOW = 0;
54 private const MAILBOX_ENCODING =
'UTF-8';
56 private ?
Imap $client;
59 private function getImapClient(
int $mailboxId): ?
Imap
61 $mailbox = MailboxTable::getRow([
75 if (is_null($mailbox))
80 $mailboxTls = $mailbox[
'USE_TLS'];
84 (
int) $mailbox[
'PORT'],
85 ($mailboxTls ===
'Y' || $mailboxTls ===
'S'),
86 ($mailboxTls ===
'Y'),
95 return 'mail/message_attachment/'.date(
'Y-m-d');
111 if (!is_null($message))
113 $dir = MailboxDirectoryTable::getRow([
119 '=DIR_MD5' => $message[
'DIR_MD5'],
120 '=MAILBOX_ID' => $mailboxId,
126 $this->message =
new MessageStructure($mailboxId,
$dir[
'PATH'], $message[
'MSG_UID'], (
int) $message[
'MESSAGE_ID']);
130 $this->client = $this->getImapClient($mailboxId);
136 $structure = $this->client->fetch(
true, $messageStructure->dirPath, $messageStructure->uid,
'(BODYSTRUCTURE)',
$error);
138 if (
$error || empty($structure))
151 $extension = array_search($attachmentType, MimeType::getMimeTypeList(),
true);
168 $bodyStructure = $this->downloadBodyStructure($messageStructure);
170 if (is_null($bodyStructure))
175 $parts = $this->downloadMessageParts($messageStructure->dirPath, $messageStructure->uid, $bodyStructure, self::MESSAGE_PARTS_ATTACHMENT);
177 $bodyStructure->traverse(
178 function (
Imap\
BodyStructure $item) use (&$parts, &$attachments, $messageStructure)
180 static $attachmentIndex = 0;
182 if ($item->isMultipart() || $item->isBodyText())
191 $parts[sprintf(
'BODY[%s.MIME]', $item->getNumber())],
192 self::MAILBOX_ENCODING,
194 $parts[sprintf(
'BODY[%s]', $item->getNumber())],
195 self::MAILBOX_ENCODING,
205 $attachments[] =
new AttachmentStructure(
207 strlen($attachment[
'BODY']),
208 mb_strtolower($attachment[
'CONTENT-TYPE']),
210 contentId: $attachment[
'CONTENT-ID'],
218 private function saveAttachmentToDisk(AttachmentStructure $attachment): ?AttachmentStructure
220 if (empty($attachment->name))
225 $fileId = \CFile::saveFile(
227 'name' => md5($attachment->name),
228 'size' => $attachment->size,
229 'type' => $attachment->type,
230 'content' => $attachment->content,
231 'MODULE_ID' =>
'mail'
233 self::generateMessageAttachmentPath(),
236 if (!is_int($fileId))
241 $extendedAttachment = clone $attachment;
242 $extendedAttachment->diskId = $fileId;
244 if (is_null($extendedAttachment->imageWidth) && is_null($extendedAttachment->imageHeight))
246 $file = \CFile::GetFileArray($fileId);
248 if (
is_set($file[
'WIDTH']))
250 $extendedAttachment->imageWidth = (int) $file[
'WIDTH'];
253 if (
is_set($file[
'HEIGHT']))
255 $extendedAttachment->imageHeight = (int) $file[
'HEIGHT'];
260 return $extendedAttachment;
268 public function saveAttachmentsToDisk(
array $attachments,
bool $abortOnAnError =
false): Result
270 $allResult =
new Result();
274 foreach ($attachments as $attachment)
276 $extendedAttachment = $this->saveAttachmentToDisk($attachment);
277 if (!is_null($extendedAttachment))
279 $data[] = $extendedAttachment;
281 else if ($abortOnAnError)
283 $allResult->addError(
new Error(
'File upload error',
'FILE_UPLOAD_ERROR'));
284 $allResult->setData(
$data);
289 $allResult->setData(
$data);
298 private function getSynchronized(MessageStructure $messageStructure) :
array
302 if (empty($messageStructure->attachmentsCount))
307 $list = MailMessageAttachmentTable::getList([
318 '=MESSAGE_ID' => $messageStructure->id,
322 while ($item = $list->fetch())
324 $attachments[] =
new AttachmentStructure(
327 $item[
'CONTENT_TYPE'],
330 $item[
'IMAGE_WIDTH'],
331 $item[
'IMAGE_HEIGHT'],
332 attachmentId: $item[
'ID'],
344 private function deleteAttachedFromDB(MessageStructure $messageStructure,
array $attachments) : void
349 foreach ($attachments as $attachment)
351 $ids[] = $attachment->attachmentId;
354 if ($messageStructure->attachmentsCount > 0)
360 MailMessageAttachmentTable::deleteByIds(
$messageId, $ids);
370 private function saveAttachmentsToDB(MessageStructure $messageStructure,
array $attachments):
array
372 $newAttachments = [];
375 foreach ($attachments as $attachment)
377 if (!is_null($attachment->diskId))
379 $result = MailMessageAttachmentTable::add(
381 'MESSAGE_ID' => $messageStructure->id,
382 'FILE_ID' => $attachment->diskId,
383 'FILE_NAME' => $attachment->name,
384 'FILE_SIZE' => $attachment->size,
386 'CONTENT_TYPE' => $attachment->type,
387 'IMAGE_WIDTH' => $attachment->imageWidth,
388 'IMAGE_HEIGHT' => $attachment->imageHeight,
392 $primary =
$result->getPrimary();
394 if (isset($primary[
'ID']))
396 $newAttachment = clone $attachment;
397 $newAttachment->attachmentId = $primary[
'ID'];
398 $newAttachments[] = $newAttachment;
403 return $newAttachments;
406 private function createMessageWithBody(MessageStructure $messageStructure): MessageStructure
408 $extendedStructure = clone $messageStructure;
410 $modelRow = MailMessageTable::getRow([
415 'ID' => $messageStructure->id,
420 isset($modelRow[
'BODY_HTML']) &&
421 is_string($modelRow[
'BODY_HTML'])
424 $extendedStructure->body = $modelRow[
'BODY_HTML'];
427 return $extendedStructure;
434 $body = $messageStructure->body;
438 return $attachmentIds;
441 $pattern =
'/<img[^>]+src\s*=\s*(\'|\")?aid:(?P<id>\d+)\s*\1[^>]*>/is';
445 $attachmentIds = array_map(
'intval',
$matches[
'id']);
448 return $attachmentIds;
460 if (
count($attachmentIds) === 0)
465 $attachmentList = MailMessageAttachmentTable::getList([
476 '@ID' => $attachmentIds,
477 '=MESSAGE_ID' => $messageStructure->id,
481 while ($attachment = $attachmentList->fetch())
484 $attachment[
'FILE_NAME'],
485 $attachment[
'FILE_SIZE'],
486 $attachment[
'CONTENT_TYPE'],
487 diskId: (
int) $attachment[
'FILE_ID'],
488 imageWidth: (
int) $attachment[
'IMAGE_WIDTH'],
489 imageHeight: (
int) $attachment[
'IMAGE_HEIGHT'],
490 attachmentId: (
int) $attachment[
'ID']
501 private function getAttachmentsEmbeddedInMessageBody(MessageStructure $messageStructure) :
array
507 if (!empty($fileIds))
509 $attachments = $this->getAttachmentStructuresByIds($messageStructure, $fileIds);
515 private static function compareAttachment(AttachmentStructure $brokenAttachment, AttachmentStructure $fullAttachment,
int $comparisonLevel = self::COMPARISON_ATTACHMENT_LEVEL_STRONG,
bool $attachmentIsPicture =
true): bool
517 if ($attachmentIsPicture && !\CFile::isImage($fullAttachment->name, $fullAttachment->type))
523 $comparisonLevel === self::COMPARISON_ATTACHMENT_LEVEL_STRONG &&
524 $brokenAttachment->name === $fullAttachment->name &&
525 $brokenAttachment->type === $fullAttachment->type
532 $comparisonLevel === self::COMPARISON_ATTACHMENT_LEVEL_AVERAGE &&
533 $brokenAttachment->name === $fullAttachment->name &&
534 $brokenAttachment->size === $fullAttachment->size
541 $comparisonLevel === self::COMPARISON_ATTACHMENT_LEVEL_LOW &&
542 $brokenAttachment->name === $fullAttachment->name
557 private function createMessageWithReplacedAttachmentsInBody(MessageStructure $messageStructure,
array $oldAttachments,
array $newAttachments) : MessageStructure
559 $extendedStructure = clone $messageStructure;
562 self::COMPARISON_ATTACHMENT_LEVEL_STRONG,
563 self::COMPARISON_ATTACHMENT_LEVEL_AVERAGE,
564 self::COMPARISON_ATTACHMENT_LEVEL_LOW
567 $remainingOldAttachments = [];
568 $remainingNewAttachments = $newAttachments;
571 foreach ($oldAttachments as $oldAttachment)
576 foreach ($remainingNewAttachments as $newKey => $newAttachment)
578 if ($this->compareAttachment($oldAttachment, $newAttachment, $level))
580 $oldId = $oldAttachment->attachmentId;
581 $newId = $newAttachment->attachmentId;
583 if ($oldId !==
null && $newId !==
null && !is_null($extendedStructure->body))
585 $pattern =
'/(src\s*=\s*["\']?aid:)' . $oldId .
'(["\']?)/i';
586 $replacement =
'src="aid:'.$newId.
'"';
587 $newBody = preg_replace(
$pattern, $replacement, $extendedStructure->body);
589 if (!is_null($newBody))
591 $extendedStructure->body = $newBody;
594 unset($remainingNewAttachments[$newKey]);
603 $remainingOldAttachments[] = $oldAttachment;
607 $oldAttachments = $remainingOldAttachments;
608 $newAttachments = $remainingNewAttachments;
611 return $extendedStructure;
614 public function update(): bool
616 if (is_null($this->message) || is_null($this->client))
621 $attachmentStructures = $this->downloadAttachments($this->message);
622 $savingToDiskResult = $this->saveAttachmentsToDisk($attachmentStructures,
true);
623 $attachmentStructures = $savingToDiskResult->getData();
625 if ($savingToDiskResult->isSuccess() ===
false)
628 foreach ($attachmentStructures as $attachment)
630 if (is_int($attachment->diskId))
632 \CFile::Delete($attachment->diskId);
639 $this->message = $this->createMessageWithAttachmentCount($this->message);
641 $oldAttachments = $this->getSynchronized($this->message);
644 foreach ($oldAttachments as $attachment)
646 $diskId = $attachment->diskId;
650 \CFile::Delete($diskId);
654 $newAttachments = $savingToDiskResult->getData();
655 $newAttachments = $this->saveAttachmentsToDB($this->message, $newAttachments);
657 $this->message = $this->createMessageWithBody($this->message);
659 $attachmentsEmbeddedInMessageBody = $this->getAttachmentsEmbeddedInMessageBody($this->message);
661 $messageWithUpdatedBody = $this->createMessageWithReplacedAttachmentsInBody($this->message, $attachmentsEmbeddedInMessageBody, $newAttachments);
663 if ($this->message->body !== $messageWithUpdatedBody->body)
665 $this->message->body = $messageWithUpdatedBody->body;
667 MailMessageTable::update(
670 'BODY_HTML' => $this->message->body,
675 $this->deleteAttachedFromDB($this->message, $oldAttachments);
677 $messageWithNewAttachmentCount = $this->createMessageWithAttachmentCount($this->message,
count($newAttachments));
679 if ($messageWithNewAttachmentCount->attachmentsCount !== $this->message->attachmentsCount)
681 $this->message->attachmentsCount = $messageWithNewAttachmentCount->attachmentsCount;
683 MailMessageTable::update(
686 'ATTACHMENTS' => $this->message->attachmentsCount,
701 private function createMessageWithAttachmentCount(MessageStructure $messageStructure, ?
int $attachmentCount =
null): MessageStructure
703 $extendedStructure = clone $messageStructure;
705 if (is_null($attachmentCount))
707 $modelRow = MailMessageTable::getRow([
712 'ID' => $messageStructure->id,
716 if (isset($modelRow[
'ATTACHMENTS']))
718 $extendedStructure->attachmentsCount = (int) $modelRow[
'ATTACHMENTS'];
722 $extendedStructure->attachmentsCount = 0;
727 $extendedStructure->attachmentsCount = $attachmentCount;
730 return $extendedStructure;
733 private function downloadMessageParts(
string $dirPath,
string $uid, Imap\BodyStructure $bodyStructure,
int $type = self::MESSAGE_PARTS_ALL):
array
735 $messagePartsMetadata = [];
737 $fetchCommands = array_filter(
738 $bodyStructure->traverse(
739 function (Imap\BodyStructure $item) use (
$type, &$messagePartsMetadata)
741 if ($item->isMultipart())
746 $isTextItem = $item->isBodyText();
748 if (
$type === ($isTextItem ? self::MESSAGE_PARTS_TEXT : self::MESSAGE_PARTS_ATTACHMENT))
751 if ($item->getType() ===
'message' && $item->getSubtype() ===
'rfc822')
753 $messagePartsMetadata[] = $item;
755 return sprintf(
'BODY.PEEK[%1$s.HEADER] BODY.PEEK[%1$s.TEXT]', $item->getNumber());
758 return sprintf(
'BODY.PEEK[%1$s.MIME] BODY.PEEK[%1$s]', $item->getNumber());
766 if (empty($fetchCommands))
773 $fetchedParts = $this->client->fetch(
777 sprintf(
'(%s)', join(
' ', $fetchCommands)),
781 if ($fetchedParts ===
false)
786 return $this->combineMessageParts($fetchedParts, $messagePartsMetadata);
799 private function combineMessageParts(
array $fetchedParts,
array $messagePartsMetadata):
array
802 foreach ($messagePartsMetadata as $item)
804 $headerKey = sprintf(
'BODY[%s.HEADER]', $item->getNumber());
805 $bodyKey = sprintf(
'BODY[%s.TEXT]', $item->getNumber());
807 if (array_key_exists($headerKey, $fetchedParts) || array_key_exists($bodyKey, $fetchedParts))
809 $partMime =
'Content-Type: message/rfc822';
811 if (!empty($item->getParams()[
'name']))
813 $partMime .= sprintf(
'; name="%s"', $item->getParams()[
'name']);
816 if (!empty($item->getDisposition()[0]))
818 $partMime .= sprintf(
"\r\nContent-Disposition: %s", $item->getDisposition()[0]);
820 if (!empty($item->getDisposition()[1]) && is_array($item->getDisposition()[1]))
822 foreach ($item->getDisposition()[1] as
$name => $value)
824 $partMime .= sprintf(
'; %s="%s"',
$name, $value);
829 $fetchedParts[sprintf(
'BODY[%1$s.MIME]', $item->getNumber())] = $partMime;
831 $fetchedParts[sprintf(
'BODY[%1$s]', $item->getNumber())] = sprintf(
833 rtrim($fetchedParts[$headerKey],
"\r\n"),
834 ltrim($fetchedParts[$bodyKey],
"\r\n")
837 unset($fetchedParts[$headerKey], $fetchedParts[$bodyKey]);
841 return $fetchedParts;