diff --git a/src/Connection/ConnectionInterface.php b/src/Connection/ConnectionInterface.php index b1cade2..65cbff0 100644 --- a/src/Connection/ConnectionInterface.php +++ b/src/Connection/ConnectionInterface.php @@ -186,7 +186,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, Im /** * Send a "RFC822.SIZE" command. * - * Fetch message sizes. + * Fetch message sizes for one or more messages. * * @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.21 */ diff --git a/src/FileMessage.php b/src/FileMessage.php index 49577b9..eee2ab8 100644 --- a/src/FileMessage.php +++ b/src/FileMessage.php @@ -24,6 +24,14 @@ public function uid(): int throw new BadMethodCallException('FileMessage does not support a UID'); } + /** + * {@inheritDoc} + */ + public function size(): ?int + { + return strlen($this->contents); + } + /** * {@inheritDoc} */ diff --git a/src/Message.php b/src/Message.php index 492d6bd..3cf2233 100644 --- a/src/Message.php +++ b/src/Message.php @@ -22,6 +22,7 @@ public function __construct( protected array $flags, protected string $head, protected string $body, + protected ?int $size = null, ) {} /** @@ -30,7 +31,7 @@ public function __construct( public function __sleep(): array { // We don't want to serialize the parsed message. - return ['folder', 'uid', 'flags', 'headers', 'contents']; + return ['folder', 'uid', 'flags', 'headers', 'contents', 'size']; } /** @@ -49,6 +50,14 @@ public function uid(): int return $this->uid; } + /** + * Get the message's size in bytes (RFC822.SIZE). + */ + public function size(): ?int + { + return $this->size; + } + /** * Get the message's flags. */ @@ -203,6 +212,7 @@ public function toArray(): array 'flags' => $this->flags, 'head' => $this->head, 'body' => $this->body, + 'size' => $this->size, ]; } diff --git a/src/MessageInterface.php b/src/MessageInterface.php index 54b7737..2ece177 100644 --- a/src/MessageInterface.php +++ b/src/MessageInterface.php @@ -15,6 +15,11 @@ interface MessageInterface extends FlaggableInterface, Stringable */ public function uid(): int; + /** + * Get the message's size in bytes (RFC822.SIZE). + */ + public function size(): ?int; + /** * Get the message date and time. */ diff --git a/src/MessageQuery.php b/src/MessageQuery.php index e7f464d..06170f4 100644 --- a/src/MessageQuery.php +++ b/src/MessageQuery.php @@ -239,6 +239,7 @@ protected function populate(Collection $uids): MessageCollection $response['flags'] ?? [], $response['headers'] ?? '', $response['contents'] ?? '', + $response['size'] ?? null, ) ); } @@ -264,6 +265,10 @@ protected function fetch(Collection $messages): array $fetch[] = 'FLAGS'; } + if ($this->fetchSize) { + $fetch[] = 'RFC822.SIZE'; + } + if ($this->fetchBody) { $fetch[] = $this->fetchAsUnread ? 'BODY.PEEK[TEXT]' @@ -279,6 +284,7 @@ protected function fetch(Collection $messages): array if (empty($fetch)) { return $uids->mapWithKeys(fn (string|int $uid) => [ $uid => [ + 'size' => null, 'flags' => [], 'headers' => '', 'contents' => '', @@ -299,8 +305,11 @@ protected function fetch(Collection $messages): array $uid = $data->lookup('UID')->value; + $size = $data->lookup('RFC822.SIZE')?->value; + return [ $uid => [ + 'size' => $size ? (int) $size : null, 'flags' => $data->lookup('FLAGS')?->values() ?? [], 'headers' => $data->lookup('[HEADER]')->value ?? '', 'contents' => $data->lookup('[TEXT]')->value ?? '', @@ -356,9 +365,9 @@ protected function id(int $id, ImapFetchIdentifier $identifier = ImapFetchIdenti /** * Make a new message from given raw components. */ - protected function newMessage(int $uid, array $flags, string $headers, string $contents): Message + protected function newMessage(int $uid, array $flags, string $headers, string $contents, ?int $size = null): Message { - return new Message($this->folder, $uid, $flags, $headers, $contents); + return new Message($this->folder, $uid, $flags, $headers, $contents, $size); } /** diff --git a/src/MessageQueryInterface.php b/src/MessageQueryInterface.php index 10ddc59..23892ba 100644 --- a/src/MessageQueryInterface.php +++ b/src/MessageQueryInterface.php @@ -61,6 +61,11 @@ public function isFetchingFlags(): bool; */ public function isFetchingHeaders(): bool; + /** + * Determine if the size of messages is being fetched. + */ + public function isFetchingSize(): bool; + /** * Fetch the flags of messages. */ @@ -76,6 +81,11 @@ public function withBody(): MessageQueryInterface; */ public function withHeaders(): MessageQueryInterface; + /** + * Fetch the size of messages. + */ + public function withSize(): MessageQueryInterface; + /** * Don't fetch the body of messages. */ @@ -91,6 +101,11 @@ public function withoutHeaders(): MessageQueryInterface; */ public function withoutFlags(): MessageQueryInterface; + /** + * Don't fetch the size of messages. + */ + public function withoutSize(): MessageQueryInterface; + /** * Set the fetch order. */ diff --git a/src/QueriesMessages.php b/src/QueriesMessages.php index 660fc22..1c4845b 100644 --- a/src/QueriesMessages.php +++ b/src/QueriesMessages.php @@ -40,6 +40,11 @@ trait QueriesMessages */ protected bool $fetchHeaders = false; + /** + * Whether to fetch the message size. + */ + protected bool $fetchSize = false; + /** * The fetch order. * @@ -165,6 +170,14 @@ public function isFetchingHeaders(): bool return $this->fetchHeaders; } + /** + * {@inheritDoc} + */ + public function isFetchingSize(): bool + { + return $this->fetchSize; + } + /** * {@inheritDoc} */ @@ -189,6 +202,14 @@ public function withHeaders(): MessageQueryInterface return $this->setFetchHeaders(true); } + /** + * {@inheritDoc} + */ + public function withSize(): MessageQueryInterface + { + return $this->setFetchSize(true); + } + /** * {@inheritDoc} */ @@ -213,6 +234,14 @@ public function withoutFlags(): MessageQueryInterface return $this->setFetchFlags(false); } + /** + * {@inheritDoc} + */ + public function withoutSize(): MessageQueryInterface + { + return $this->setFetchSize(false); + } + /** * Set whether to fetch the flags. */ @@ -243,6 +272,16 @@ protected function setFetchHeaders(bool $fetchHeaders): MessageQueryInterface return $this; } + /** + * Set whether to fetch the size. + */ + protected function setFetchSize(bool $fetchSize): MessageQueryInterface + { + $this->fetchSize = $fetchSize; + + return $this; + } + /** {@inheritDoc} */ public function setFetchOrder(string $fetchOrder): MessageQueryInterface { diff --git a/src/Testing/FakeMessage.php b/src/Testing/FakeMessage.php index 2a93be2..4715d69 100644 --- a/src/Testing/FakeMessage.php +++ b/src/Testing/FakeMessage.php @@ -19,6 +19,7 @@ public function __construct( protected int $uid, protected array $flags = [], protected string $contents = '', + protected ?int $size = null, ) {} /** @@ -29,6 +30,14 @@ public function uid(): int return $this->uid; } + /** + * {@inheritDoc} + */ + public function size(): int + { + return $this->size ?? strlen($this->contents); + } + /** * {@inheritDoc} */ diff --git a/tests/Integration/MessagesTest.php b/tests/Integration/MessagesTest.php index 4e66426..096abf4 100644 --- a/tests/Integration/MessagesTest.php +++ b/tests/Integration/MessagesTest.php @@ -134,8 +134,60 @@ function folder(): Folder fn (MessageQuery $query) => $query->withBody(), fn (MessageQuery $query) => $query->withFlags(), fn (MessageQuery $query) => $query->withHeaders(), + fn (MessageQuery $query) => $query->withSize(), ]); +test('get with size', function () { + $folder = folder(); + + $uid = $folder->messages()->append( + new DraftMessage( + from: 'foo@email.com', + to: 'bar@email.com', + subject: 'Test Subject', + text: 'hello world', + ), + ); + + // Fetch without size - should be null + $messagesWithoutSize = $folder->messages()->get(); + expect($messagesWithoutSize->first()->size())->toBeNull(); + + // Fetch with size - should have a value + $messagesWithSize = $folder->messages()->withSize()->get(); + $message = $messagesWithSize->first(); + + expect($message->size())->toBeInt(); + expect($message->size())->toBeGreaterThan(0); + expect($message->uid())->toBe($uid); +}); + +test('size reflects actual message size', function () { + $folder = folder(); + + $shortMessage = new DraftMessage( + from: 'foo@email.com', + text: 'short', + ); + + $longMessage = new DraftMessage( + from: 'foo@email.com', + text: str_repeat('This is a longer message with more content. ', 100), + ); + + $uid1 = $folder->messages()->append($shortMessage); + $uid2 = $folder->messages()->append($longMessage); + + $messages = $folder->messages()->withSize()->get(); + + $short = $messages->find($uid1); + $long = $messages->find($uid2); + + expect($short->size())->toBeInt(); + expect($long->size())->toBeInt(); + expect($long->size())->toBeGreaterThan($short->size()); +}); + test('append', function () { $folder = folder(); diff --git a/tests/Unit/FileMessageTest.php b/tests/Unit/FileMessageTest.php index d37cab0..825f70d 100644 --- a/tests/Unit/FileMessageTest.php +++ b/tests/Unit/FileMessageTest.php @@ -481,3 +481,18 @@ // Different content expect($message1->is($message3))->toBeFalse(); }); + +test('it can determine size from contents', function () { + $contents = <<<'EOT' + From: "John Doe" + Subject: Test Subject + Date: Wed, 19 Feb 2025 12:34:56 -0500 + Content-Type: text/plain; charset="UTF-8" + + Test content + EOT; + + $message = new FileMessage($contents); + + expect($message->size())->toBe(strlen($contents)); +}); diff --git a/tests/Unit/Testing/FakeMessageTest.php b/tests/Unit/Testing/FakeMessageTest.php index 84981cc..4546ed4 100644 --- a/tests/Unit/Testing/FakeMessageTest.php +++ b/tests/Unit/Testing/FakeMessageTest.php @@ -118,3 +118,15 @@ expect($message->isFlagged())->toBeFalse(); expect($message->flags())->toBeEmpty(); }); + +test('it can get size when set', function () { + $message = new FakeMessage(1, [], 'Test content', 1024); + + expect($message->size())->toBe(1024); +}); + +test('it returns size from contents when size is not set', function () { + $message = new FakeMessage(1, [], 'Test content'); + + expect($message->size())->toBe(strlen('Test content')); +});