From 6bb54be5a3bf303138870248818b084dfb9d1cb5 Mon Sep 17 00:00:00 2001 From: OpenXE <> Date: Thu, 2 Feb 2023 16:39:00 +0100 Subject: [PATCH] Ticket system rewrite of ticket import attachment handling, now with application/octet-stream and cid --- .../MailClient/Data/MailAttachmentData.php | 125 +++++++++--------- .../MailClient/Data/MailMessageData.php | 17 ++- .../MailClient/Data/MailMessagePartData.php | 12 ++ .../Ticket/Task/TicketImportHelper.php | 6 +- 4 files changed, 85 insertions(+), 75 deletions(-) diff --git a/classes/Components/MailClient/Data/MailAttachmentData.php b/classes/Components/MailClient/Data/MailAttachmentData.php index d2c97db0..741c6a1f 100644 --- a/classes/Components/MailClient/Data/MailAttachmentData.php +++ b/classes/Components/MailClient/Data/MailAttachmentData.php @@ -51,9 +51,35 @@ class MailAttachmentData implements MailAttachmentInterface $this->cid = $cid; } + + /* + Check the type of Attachment + Possible results: application/octet-stream, attachment, inline + */ + public static function getAttachmentPartType(MailMessagePartInterface $part): ?string { + + if (!$part->isMultipart()) { + $header = $part->getHeader('content-disposition'); + if ($header !== null) { + $split = explode(';', $header->getValue()); + if ($split[0] === 'attachment') { + return ('attachment'); + } else if ($split[0] === 'inline') { + return ('inline'); + } + } else { // Check for application/octet-stream + $content_type = $part->getContentType(); + if ($content_type == 'application/octet-stream') { + return('application/octet-stream'); + } + } + } + + return(null); + } + /** * @param MailMessagePartInterface $part - * * @throws InvalidArgumentException * * @return MailAttachmentData @@ -61,21 +87,22 @@ class MailAttachmentData implements MailAttachmentInterface public static function fromMailMessagePart(MailMessagePartInterface $part): MailAttachmentData { - $isInline = false; + $attachmenttype = MailAttachmentData::getAttachmentPartType($part); - $encodingHeader = $part->getHeader('content-transfer-encoding'); - if ($encodingHeader === null) { - // Assume this is no error (?) throw new InvalidArgumentException('missing header: "Content-Transfer-Encoding"'); - $encoding = ''; - } else { - $encoding = $encodingHeader->getValue(); + if ($attachmenttype == null) { + throw new InvalidArgumentException('object is no attachment'); } - $dispositionHeader = $part->getHeader('content-disposition'); - if ($dispositionHeader === null) { - throw new InvalidArgumentException('missing header: "Content-Disposition"'); - } - $disposition = $dispositionHeader->getValue(); + $disposition = $part->getHeaderValue('content-disposition'); + if ($disposition == null) { + $disposition = ''; + } + + $disposition = str_replace(["\n\r", "\n", "\r"], '', $disposition); + + // file_put_contents('debug.txt',date("HH:mm:ss")."\nDispo: ".$disposition); // FILE_APPEND + + // Determine filename /* Content-Disposition: inline Content-Disposition: attachment @@ -84,60 +111,29 @@ class MailAttachmentData implements MailAttachmentInterface This is not correctly implemented -> only the first string is evaluated Content-Disposition: attachment; filename*0="filename_that_is_" Content-Disposition: attachment; filename*1="very_long.jpg" - */ - - if (preg_match('/(.+);\s*filename(?:\*[0-9]){0,1}="([^"]+)".*$/m', $disposition, $matches)) { - $isInline = strtolower($matches[1]) === 'inline'; + $filename = 'OpenXE_file.unknown'; + if (preg_match('/(.+);\s*filename(?:\*[0-9]){0,1}="*([^"]+)"*.*$/m', $disposition, $matches)) { // Filename in disposition $filename = $matches[2]; - } - else if ($disposition == 'attachment') { - // Filename is given in Content-Type e.g. - /* Content-Type: application/pdf; name="Filename.pdf" - Content-Transfer-Encoding: base64 - Content-Disposition: attachment - */ + } else { + $contenttype = $part->getHeaderValue('content-type'); - $contenttypeHeader = $part->getHeader('content-type'); - if ($contenttypeHeader === null) { - throw new InvalidArgumentException('missing header: "Content-Type"'); - } - $contenttype = $contenttypeHeader->getValue(); + $contenttype = str_replace(["\n\r", "\n", "\r"], '', $contenttype); - if (preg_match('/(.+);\s*name(?:\*[0-9]){0,1}="([^"]+)".*$/m', $contenttype, $matches)) { - $isInline = strtolower($matches[1]) === 'inline'; +// file_put_contents('debug.txt',date("HH:mm:ss")."\nConttype: ".$contenttype,FILE_APPEND); // FILE_APPEND + + if (preg_match('/(.+);\s*name(?:\*[0-9]){0,1}="*([^"]+)"*.*$/m', $contenttype, $matches)) { // Name in content-type $filename = $matches[2]; - } else { - throw new InvalidArgumentException( - sprintf('missing filename in header value "Content-Type" = "%s"', $contenttype) - ); + } else if ($contenttype == 'message/rfc822') { // RFC822 message + $filename = 'ForwardedMessage.eml'; } } - else if ($disposition == 'inline') { - $isInline = true; - $filename = "OpenXE_file.inline"; - } - else if (strpos($disposition,'attachment;\n') == 0) { // No filename, check for content type message/rfc822 - - $contenttypeHeader = $part->getHeader('content-type'); - if ($contenttypeHeader === null) { - throw new InvalidArgumentException('missing header: "Content-Type"'); - } - $contenttype = $contenttypeHeader->getValue(); - if ($contenttype == 'message/rfc822') { - $filename = 'ForwardedMessage.eml'; - } else { - /* throw new InvalidArgumentException( - sprintf('unexpected header value "Content-Disposition" = "%s"', $disposition) - );*/ - $filename = "OpenXE_file.unknown"; - } - } - else { - throw new InvalidArgumentException( - sprintf('unexpected header value "Content-Disposition" = "%s", not message/rfc822', $disposition) - ); + $encodingHeader = $part->getHeader('content-transfer-encoding'); + if ($encodingHeader === null) { + $content_transfer_encoding = ''; + } else { + $content_transfer_encoding = $encodingHeader->getValue(); } // Thunderbird UTF URL-Format @@ -147,8 +143,7 @@ class MailAttachmentData implements MailAttachmentInterface $filename = substr($filename,$UTF_pos); $filename = rawurldecode($filename); } - - + $cid = null; $contentIdHeader = $part->getHeader('content-id'); if ($contentIdHeader !== null) { @@ -157,9 +152,13 @@ class MailAttachmentData implements MailAttachmentInterface $cid = $cidMatches[1]; } } + if ($attachmenttype == 'inline' && $cid != null) { + $filename = "cid:".$cid; + } $content = $part->getContent(); if ($content === null) { // This should not be +// file_put_contents('debug.txt',date("HH:mm:ss")."\n".print_r($part,true)); // FILE_APPEND throw new InvalidArgumentException( sprintf('content is null "%s"', substr(print_r($part,true),0,1000)) ); @@ -169,8 +168,8 @@ class MailAttachmentData implements MailAttachmentInterface $filename, $content, $part->getContentType(), - $encoding, - $isInline, + $content_transfer_encoding, + $attachmenttype == 'inline', $cid ); } diff --git a/classes/Components/MailClient/Data/MailMessageData.php b/classes/Components/MailClient/Data/MailMessageData.php index f0544ce6..88224c88 100644 --- a/classes/Components/MailClient/Data/MailMessageData.php +++ b/classes/Components/MailClient/Data/MailMessageData.php @@ -147,6 +147,7 @@ final class MailMessageData implements MailMessageInterface, JsonSerializable $parts = []; $this->findAttachmentParts($this->contentPart, $parts); $attachments = []; + foreach ($parts as $part) { $attachments[] = MailAttachmentData::fromMailMessagePart($part); } @@ -161,19 +162,17 @@ final class MailMessageData implements MailMessageInterface, JsonSerializable * @return void */ private function findAttachmentParts(MailMessagePartInterface $part, array &$resultArray): void - { - try { - $header = $part->getHeader('content-disposition'); - $split = explode(';', $header->getValue()); - if ($split[0] === 'attachment' || $split[0] === 'inline') { - $resultArray[] = $part; + { - return; - } - } catch (Throwable $e) { + if ($part->isMultipart()) { + // Recurse subparts for ($i = 0; $i < $part->countParts(); $i++) { $this->findAttachmentParts($part->getPart($i), $resultArray); } + } else { + if (MailAttachmentData::getAttachmentPartType($part) != null) { + $resultArray[] = $part; + } } } diff --git a/classes/Components/MailClient/Data/MailMessagePartData.php b/classes/Components/MailClient/Data/MailMessagePartData.php index c974ebad..cc55fe8d 100644 --- a/classes/Components/MailClient/Data/MailMessagePartData.php +++ b/classes/Components/MailClient/Data/MailMessagePartData.php @@ -83,6 +83,18 @@ final class MailMessagePartData implements MailMessagePartInterface, JsonSeriali return $this->headers[strtolower($name)]; } + /** + * @inheritDoc + */ + public function getHeaderValue(string $name): ?string + { + $header = $this->getHeader($name); + if ($header == null) { + return (null); + } + return($header->getValue()); + } + /** * @inheritDoc */ diff --git a/classes/Modules/Ticket/Task/TicketImportHelper.php b/classes/Modules/Ticket/Task/TicketImportHelper.php index c58426b6..36a29c6a 100644 --- a/classes/Modules/Ticket/Task/TicketImportHelper.php +++ b/classes/Modules/Ticket/Task/TicketImportHelper.php @@ -495,7 +495,7 @@ class TicketImportHelper try { // $this->logger->debug('Start import', ['message' => substr(print_r($message,true),1000)]); - $this->logger->debug('Start import', []); + $this->logger->debug('Start import '.$messageNumber, []); $result = $this->importMessage($message); @@ -507,11 +507,11 @@ class TicketImportHelper $this->mailClient->setFlags((int)$messageNumber, ['\\Seen']); } } else { - $this->logger->error('Error during email import', ['message' => substr(print_r($message,true),0,1000)]); + $this->logger->error('Error during email import '.$messageNumber, ['message' => substr(print_r($message,true),0,1000)]); continue; } } catch (Throwable $e) { - $this->logger->error('Error during email import', ['message' => substr(print_r($message,true),0,1000)]); + $this->logger->error('Error during email import '.$messageNumber, ['message' => substr(print_r($message,true),0,1000)]); continue; } }