Repository: DirectoryTree/ImapEngine (PHP)

Issue:
Provide RFC822.SIZE as part of Message object
It would be really convenient, if I could just ask the Message object for the entire size of the mail, without having to fetch the entire mail or manually go through the ConnectionInterface


Discussion:

Patch:
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}
      */


Test patch:
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" <john@example.com>
+    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'));
+});