From 92647fd61572b258f2804f5721f1c4c1911289e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bou=C4=8Dek?= Date: Thu, 16 Feb 2023 11:30:38 +0100 Subject: [PATCH] Messages: Add ability to native save mail content to file (#556) --- src/Message.php | 8 +++ src/Message/AbstractPart.php | 20 ++++++ src/Message/BasicMessageInterface.php | 7 +++ src/Message/EmbeddedMessage.php | 8 +++ tests/EmbeddedMessageTest.php | 28 +++++++++ tests/MessageTest.php | 39 ++++++++++++ ...l_without_content_disposition-embedded.eml | 61 +++++++++++++++++++ 7 files changed, 171 insertions(+) create mode 100644 tests/fixtures/embedded_email_without_content_disposition-embedded.eml diff --git a/src/Message.php b/src/Message.php index bc2c10cb..4d47231c 100644 --- a/src/Message.php +++ b/src/Message.php @@ -133,6 +133,14 @@ public function getRawMessage(): string return $this->rawMessage; } + /** + * @param resource|string $file the path to the saved file as a string, or a valid file descriptor + */ + public function saveRawMessage($file): void + { + $this->doSaveContent($file, ''); + } + public function getHeaders(): Message\Headers { if (null === $this->headers) { diff --git a/src/Message/AbstractPart.php b/src/Message/AbstractPart.php index ed783c75..6fa842a2 100644 --- a/src/Message/AbstractPart.php +++ b/src/Message/AbstractPart.php @@ -256,6 +256,26 @@ final protected function doGetContent(string $partNumber): string return $return; } + /** + * Save raw message content to file. + * + * @param resource|string $file the path to the saved file as a string, or a valid file descriptor + */ + final protected function doSaveContent($file, string $partNumber): void + { + $return = \imap_savebody( + $this->resource->getStream(), + $file, + $this->getNumber(), + $partNumber, + \FT_UID | \FT_PEEK + ); + + if (false === $return) { + throw new ImapFetchbodyException('imap_savebody failed'); + } + } + final public function getParts(): array { $this->lazyParseStructure(); diff --git a/src/Message/BasicMessageInterface.php b/src/Message/BasicMessageInterface.php index 83dbd17f..9d91ded8 100644 --- a/src/Message/BasicMessageInterface.php +++ b/src/Message/BasicMessageInterface.php @@ -18,6 +18,13 @@ public function getRawHeaders(): string; */ public function getRawMessage(): string; + /** + * Save the raw message, including all headers, parts, etc. unencoded and unparsed to file. + * + * @param resource|string $file the path to the saved file as a string, or a valid file descriptor + */ + public function saveRawMessage($file): void; + /** * Get message headers. */ diff --git a/src/Message/EmbeddedMessage.php b/src/Message/EmbeddedMessage.php index 20d9715f..80f542b2 100644 --- a/src/Message/EmbeddedMessage.php +++ b/src/Message/EmbeddedMessage.php @@ -38,6 +38,14 @@ public function getRawMessage(): string return $this->rawMessage; } + /** + * @param resource|string $file the path to the saved file as a string, or a valid file descriptor + */ + public function saveRawMessage($file): void + { + $this->doSaveContent($file, $this->getPartNumber()); + } + /** * Get content part number. */ diff --git a/tests/EmbeddedMessageTest.php b/tests/EmbeddedMessageTest.php index 9950605c..aa7fcdcf 100644 --- a/tests/EmbeddedMessageTest.php +++ b/tests/EmbeddedMessageTest.php @@ -162,4 +162,32 @@ public function testEmbeddedMessageWithoutContentDisposition(): void static::assertNotEmpty($attachment->getContent()); static::assertSame('file4.zip', $attachment->getFilename()); } + + public function testSaveEmbeddedMessage(): void + { + $mailbox = $this->createMailbox(); + $raw = $this->getFixture('embedded_email_without_content_disposition'); + $mailbox->addMessage($raw); + + $message = $mailbox->getMessage(1); + $attachments = $message->getAttachments(); + + // skip 1. non-embedded attachment (file.jpg) to embedded one + $attachment = \next($attachments); + static::assertNotFalse($attachment); + static::assertTrue($attachment->isEmbeddedMessage()); + + $embeddedMessage = $attachment->getEmbeddedMessage(); + + $file = \fopen('php://temp', 'w+'); + if (false === $file) { + static::fail('Unable to create temporary file stream'); + } + + $embeddedMessage->saveRawMessage($file); + \fseek($file, 0); + + $rawEmbedded = $this->getFixture('embedded_email_without_content_disposition-embedded'); + static::assertSame($rawEmbedded, \stream_get_contents($file)); + } } diff --git a/tests/MessageTest.php b/tests/MessageTest.php index ed32f153..1b81c4e5 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -748,6 +748,45 @@ public function testGetRawMessage(): void static::assertSame($fixture, $message->getRawMessage()); } + public function testSaveFileRawMessage(): void + { + $fixture = $this->getFixture('structured_with_attachment'); + $this->mailbox->addMessage($fixture); + + $message = $this->mailbox->getMessage(1); + + $filename = \tempnam(\sys_get_temp_dir(), 'testSaveFileRawMessage'); + if (false === $filename) { + static::fail('Unable to create temporary file'); + } + + $message->saveRawMessage($filename); + + static::assertSame($fixture, \file_get_contents($filename)); + + \unlink($filename); + } + + public function testSaveResourceRawMessage(): void + { + $fixture = $this->getFixture('structured_with_attachment'); + $this->mailbox->addMessage($fixture); + + $message = $this->mailbox->getMessage(1); + + $file = \fopen('php://temp', 'w+'); + if (false === $file) { + static::fail('Unable to create temporary file stream'); + } + + $message->saveRawMessage($file); + \fseek($file, 0); + + static::assertSame($fixture, \stream_get_contents($file)); + + \fclose($file); + } + public function testAttachmentOnlyEmail(): void { $fixture = $this->getFixture('mail_that_is_attachment'); diff --git a/tests/fixtures/embedded_email_without_content_disposition-embedded.eml b/tests/fixtures/embedded_email_without_content_disposition-embedded.eml new file mode 100644 index 00000000..4fc3d35a --- /dev/null +++ b/tests/fixtures/embedded_email_without_content_disposition-embedded.eml @@ -0,0 +1,61 @@ +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 12:10:49 +0200 +Subject: embedded_message_subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Language: pl-PL +Content-Type: multipart/mixed; + boundary="_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" +MIME-Version: 1.0 + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +some txt + + + + + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + + +

some txt

+ + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file1.xlsx" +Content-Description: file1.xlsx +Content-Disposition: attachment; filename="file1.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:06:01 GMT"; + modification-date="Fri, 05 Apr 2019 10:10:49 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file2.xlsx" +Content-Description: file2 +Content-Disposition: attachment; filename="file2.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:10:19 GMT"; + modification-date="Wed, 03 Apr 2019 11:04:32 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_--