From 8df62d6ccb3710550b3811f5b9b9d80886cfff67 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 1 Sep 2026 10:30:39 -0400 Subject: [PATCH 1/2] Refactor message fetch item selection --- src/Enums/ImapFetchItem.php | 26 ++++ src/FetchedMessageData.php | 73 ++++++++++ src/MessageQuery.php | 85 ++---------- src/MessageQueryInterface.php | 73 +--------- src/QueriesMessages.php | 189 +++----------------------- tests/Integration/MessagesTest.php | 49 +++---- tests/Unit/FetchedMessageDataTest.php | 31 +++++ tests/Unit/ImapFetchItemTest.php | 20 +++ tests/Unit/MessageQueryTest.php | 54 ++++++++ tests/Unit/MessageTest.php | 4 +- 10 files changed, 266 insertions(+), 338 deletions(-) create mode 100644 src/Enums/ImapFetchItem.php create mode 100644 src/FetchedMessageData.php create mode 100644 tests/Unit/FetchedMessageDataTest.php create mode 100644 tests/Unit/ImapFetchItemTest.php diff --git a/src/Enums/ImapFetchItem.php b/src/Enums/ImapFetchItem.php new file mode 100644 index 0000000..ed31154 --- /dev/null +++ b/src/Enums/ImapFetchItem.php @@ -0,0 +1,26 @@ + 'FLAGS', + self::Size => 'RFC822.SIZE', + self::Headers => $leaveUnread ? 'BODY.PEEK[HEADER]' : 'BODY[HEADER]', + self::Body => $leaveUnread ? 'BODY.PEEK[TEXT]' : 'BODY[TEXT]', + self::BodyStructure => 'BODYSTRUCTURE', + }; + } +} diff --git a/src/FetchedMessageData.php b/src/FetchedMessageData.php new file mode 100644 index 0000000..f38fcb1 --- /dev/null +++ b/src/FetchedMessageData.php @@ -0,0 +1,73 @@ +tokenAt(3); + + if (! $data instanceof ListData) { + throw new RuntimeException(sprintf( + 'Expected instance of %s at index 3 in FETCH response, got %s', + ListData::class, + get_debug_type($data) + )); + } + + return new static( + uid: (int) $data->lookup('UID')->value, + flags: $data->lookup('FLAGS')?->values() ?? [], + head: $data->lookup('[HEADER]')->value ?? '', + body: $data->lookup('[TEXT]')->value ?? '', + size: ($size = $data->lookup('RFC822.SIZE')?->value) ? (int) $size : null, + bodyStructure: ($bodyStructure = $data->lookup('BODYSTRUCTURE')) instanceof ListData + ? $bodyStructure + : null, + ); + } + + /** + * Get the message UID. + */ + public function uid(): int + { + return $this->uid; + } + + /** + * Create a message for the given folder. + */ + public function toMessage(FolderInterface $folder): Message + { + return new Message( + $folder, + $this->uid, + $this->flags, + $this->head, + $this->body, + $this->size, + $this->bodyStructure, + ); + } +} diff --git a/src/MessageQuery.php b/src/MessageQuery.php index 18eed49..414a98c 100644 --- a/src/MessageQuery.php +++ b/src/MessageQuery.php @@ -8,14 +8,13 @@ use DirectoryTree\ImapEngine\Collections\ResponseCollection; use DirectoryTree\ImapEngine\Connection\ConnectionInterface; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; -use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData; use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse; use DirectoryTree\ImapEngine\Connection\Tokens\Token; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; +use DirectoryTree\ImapEngine\Enums\ImapFetchItem; use DirectoryTree\ImapEngine\Enums\ImapFlag; use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException; use DirectoryTree\ImapEngine\Exceptions\ImapCommandException; -use DirectoryTree\ImapEngine\Exceptions\RuntimeException; use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator; use DirectoryTree\ImapEngine\Support\Str; use Illuminate\Support\Collection; @@ -326,17 +325,8 @@ protected function populate(Collection $uids): MessageCollection $messages->total($uids->count()); - foreach ($this->fetch($uids) as $uid => $response) { - $messages->push( - $this->newMessage( - $uid, - $response['flags'] ?? [], - $response['head'] ?? '', - $response['body'] ?? '', - $response['size'] ?? null, - $response['bodystructure'] ?? null, - ) - ); + foreach ($this->fetch($uids) as $data) { + $messages->push($data->toMessage($this->folder)); } return $messages; @@ -359,68 +349,21 @@ protected function fetch(Collection $messages): array $uids = $messages->forPage($this->page, $this->limit)->values(); - $fetch = []; - - if ($this->fetchFlags) { - $fetch[] = 'FLAGS'; - } - - if ($this->fetchSize) { - $fetch[] = 'RFC822.SIZE'; - } - - if ($this->fetchHeaders) { - $fetch[] = $this->fetchAsUnread - ? 'BODY.PEEK[HEADER]' - : 'BODY[HEADER]'; - } - - if ($this->fetchBody) { - $fetch[] = $this->fetchAsUnread - ? 'BODY.PEEK[TEXT]' - : 'BODY[TEXT]'; - } - - if ($this->fetchBodyStructure) { - $fetch[] = 'BODYSTRUCTURE'; - } + $fetch = array_map( + fn (ImapFetchItem $item) => $item->command($this->fetchAsUnread), + $this->fetchItems, + ); if (empty($fetch)) { return $uids->mapWithKeys(fn (string|int $uid) => [ - $uid => [ - 'size' => null, - 'flags' => [], - 'head' => '', - 'body' => '', - 'bodystructure' => null, - ], + $uid => new FetchedMessageData((int) $uid), ])->all(); } return $this->connection()->fetch($fetch, $uids->all())->mapWithKeys(function (UntaggedResponse $response) { - $data = $response->tokenAt(3); - - if (! $data instanceof ListData) { - throw new RuntimeException(sprintf( - 'Expected instance of %s at index 3 in FETCH response, got %s', - ListData::class, - get_debug_type($data) - )); - } - - $uid = $data->lookup('UID')->value; - - $size = $data->lookup('RFC822.SIZE')?->value; + $data = FetchedMessageData::fromResponse($response); - return [ - $uid => [ - 'size' => $size ? (int) $size : null, - 'flags' => $data->lookup('FLAGS')?->values() ?? [], - 'head' => $data->lookup('[HEADER]')->value ?? '', - 'body' => $data->lookup('[TEXT]')->value ?? '', - 'bodystructure' => $data->lookup('BODYSTRUCTURE'), - ], - ]; + return [$data->uid() => $data]; })->all(); } @@ -495,14 +438,6 @@ protected function id(int $id, ImapFetchIdentifier $identifier = ImapFetchIdenti } } - /** - * Make a new message from given raw components. - */ - protected function newMessage(int $uid, array $flags, string $head, string $body, ?int $size = null, ?ListData $bodystructure = null): Message - { - return new Message($this->folder, $uid, $flags, $head, $body, $size, $bodystructure); - } - /** * Get the connection instance. */ diff --git a/src/MessageQueryInterface.php b/src/MessageQueryInterface.php index 63f0df9..0aebc0e 100644 --- a/src/MessageQueryInterface.php +++ b/src/MessageQueryInterface.php @@ -7,6 +7,7 @@ use DirectoryTree\ImapEngine\Collections\MessageCollection; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; +use DirectoryTree\ImapEngine\Enums\ImapFetchItem; use DirectoryTree\ImapEngine\Enums\ImapSortKey; use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator; @@ -51,79 +52,19 @@ public function getPage(): int; public function setPage(int $page): MessageQueryInterface; /** - * Determine if the body of messages is being fetched. + * Add items to the message FETCH request. */ - public function isFetchingBody(): bool; + public function with(ImapFetchItem ...$items): static; /** - * Determine if the flags of messages is being fetched. + * Remove items from the message FETCH request. */ - public function isFetchingFlags(): bool; + public function without(ImapFetchItem ...$items): static; /** - * Determine if the headers of messages is being fetched. + * Replace the items in the message FETCH request. */ - public function isFetchingHeaders(): bool; - - /** - * Determine if the size of messages is being fetched. - */ - public function isFetchingSize(): bool; - - /** - * Determine if the body structure of messages is being fetched. - */ - public function isFetchingBodyStructure(): bool; - - /** - * Fetch the flags of messages. - */ - public function withFlags(): MessageQueryInterface; - - /** - * Fetch the body of messages. - */ - public function withBody(): MessageQueryInterface; - - /** - * Fetch the headers of messages. - */ - public function withHeaders(): MessageQueryInterface; - - /** - * Fetch the size of messages. - */ - public function withSize(): MessageQueryInterface; - - /** - * Fetch the body structure of messages. - */ - public function withBodyStructure(): MessageQueryInterface; - - /** - * Don't fetch the body of messages. - */ - public function withoutBody(): MessageQueryInterface; - - /** - * Don't fetch the headers of messages. - */ - public function withoutHeaders(): MessageQueryInterface; - - /** - * Don't fetch the flags of messages. - */ - public function withoutFlags(): MessageQueryInterface; - - /** - * Don't fetch the size of messages. - */ - public function withoutSize(): MessageQueryInterface; - - /** - * Don't fetch the body structure of messages. - */ - public function withoutBodyStructure(): MessageQueryInterface; + public function only(ImapFetchItem ...$items): static; /** * Set the fetch order. diff --git a/src/QueriesMessages.php b/src/QueriesMessages.php index 1c45b72..2a72532 100644 --- a/src/QueriesMessages.php +++ b/src/QueriesMessages.php @@ -3,6 +3,7 @@ namespace DirectoryTree\ImapEngine; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; +use DirectoryTree\ImapEngine\Enums\ImapFetchItem; use DirectoryTree\ImapEngine\Enums\ImapSortKey; use DirectoryTree\ImapEngine\Support\ForwardsCalls; use Illuminate\Support\Traits\Conditionable; @@ -27,29 +28,11 @@ trait QueriesMessages protected ?int $limit = null; /** - * Whether to fetch the message body. - */ - protected bool $fetchBody = false; - - /** - * Whether to fetch the message flags. - */ - protected bool $fetchFlags = false; - - /** - * Whether to fetch the message headers. - */ - protected bool $fetchHeaders = false; - - /** - * Whether to fetch the message size. - */ - protected bool $fetchSize = false; - - /** - * Whether to fetch the message body structure. + * The items to include in message FETCH requests. + * + * @var array */ - protected bool $fetchBodyStructure = false; + protected array $fetchItems = []; /** * The fetch order. @@ -167,171 +150,35 @@ public function setPage(int $page): MessageQueryInterface /** * {@inheritDoc} */ - public function isFetchingBody(): bool - { - return $this->fetchBody; - } - - /** - * {@inheritDoc} - */ - public function isFetchingFlags(): bool - { - return $this->fetchFlags; - } - - /** - * {@inheritDoc} - */ - public function isFetchingHeaders(): bool - { - return $this->fetchHeaders; - } - - /** - * {@inheritDoc} - */ - public function isFetchingSize(): bool - { - return $this->fetchSize; - } - - /** - * {@inheritDoc} - */ - public function isFetchingBodyStructure(): bool - { - return $this->fetchBodyStructure; - } - - /** - * {@inheritDoc} - */ - public function withFlags(): MessageQueryInterface - { - return $this->setFetchFlags(true); - } - - /** - * {@inheritDoc} - */ - public function withBody(): MessageQueryInterface - { - return $this->setFetchBody(true); - } - - /** - * {@inheritDoc} - */ - public function withHeaders(): MessageQueryInterface - { - return $this->setFetchHeaders(true); - } - - /** - * {@inheritDoc} - */ - public function withSize(): MessageQueryInterface + public function with(ImapFetchItem ...$items): static { - return $this->setFetchSize(true); - } - - /** - * {@inheritDoc} - */ - public function withBodyStructure(): MessageQueryInterface - { - return $this->setFetchBodyStructure(true); - } - - /** - * {@inheritDoc} - */ - public function withoutBody(): MessageQueryInterface - { - return $this->setFetchBody(false); - } - - /** - * {@inheritDoc} - */ - public function withoutHeaders(): MessageQueryInterface - { - return $this->setFetchHeaders(false); - } - - /** - * {@inheritDoc} - */ - public function withoutFlags(): MessageQueryInterface - { - return $this->setFetchFlags(false); - } - - /** - * {@inheritDoc} - */ - public function withoutSize(): MessageQueryInterface - { - return $this->setFetchSize(false); - } - - /** - * {@inheritDoc} - */ - public function withoutBodyStructure(): MessageQueryInterface - { - return $this->setFetchBodyStructure(false); - } - - /** - * Set whether to fetch the flags. - */ - protected function setFetchFlags(bool $fetchFlags): MessageQueryInterface - { - $this->fetchFlags = $fetchFlags; - - return $this; - } - - /** - * Set the fetch body flag. - */ - protected function setFetchBody(bool $fetchBody): MessageQueryInterface - { - $this->fetchBody = $fetchBody; - - return $this; - } - - /** - * Set whether to fetch the headers. - */ - protected function setFetchHeaders(bool $fetchHeaders): MessageQueryInterface - { - $this->fetchHeaders = $fetchHeaders; + foreach ($items as $item) { + $this->fetchItems[$item->value] = $item; + } return $this; } /** - * Set whether to fetch the size. + * {@inheritDoc} */ - protected function setFetchSize(bool $fetchSize): MessageQueryInterface + public function without(ImapFetchItem ...$items): static { - $this->fetchSize = $fetchSize; + foreach ($items as $item) { + unset($this->fetchItems[$item->value]); + } return $this; } /** - * Set whether to fetch the body structure. + * {@inheritDoc} */ - protected function setFetchBodyStructure(bool $fetchBodyStructure): MessageQueryInterface + public function only(ImapFetchItem ...$items): static { - $this->fetchBodyStructure = $fetchBodyStructure; + $this->fetchItems = []; - return $this; + return $this->with(...$items); } /** {@inheritDoc} */ diff --git a/tests/Integration/MessagesTest.php b/tests/Integration/MessagesTest.php index 15fe82e..03d7416 100644 --- a/tests/Integration/MessagesTest.php +++ b/tests/Integration/MessagesTest.php @@ -3,6 +3,7 @@ use Carbon\Carbon; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\DraftMessage; +use DirectoryTree\ImapEngine\Enums\ImapFetchItem; use DirectoryTree\ImapEngine\Folder; use DirectoryTree\ImapEngine\Message; use DirectoryTree\ImapEngine\MessageQuery; @@ -141,10 +142,10 @@ function folder(): Folder expect($messages->count())->toBe(1); expect($messages->first()->uid())->toBe($uid); })->with([ - fn (MessageQuery $query) => $query->withBody(), - fn (MessageQuery $query) => $query->withFlags(), - fn (MessageQuery $query) => $query->withHeaders(), - fn (MessageQuery $query) => $query->withSize(), + fn (MessageQuery $query) => $query->with(ImapFetchItem::Body), + fn (MessageQuery $query) => $query->with(ImapFetchItem::Flags), + fn (MessageQuery $query) => $query->with(ImapFetchItem::Headers), + fn (MessageQuery $query) => $query->with(ImapFetchItem::Size), ]); test('get with size', function () { @@ -164,7 +165,7 @@ function folder(): Folder expect($messagesWithoutSize->first()->size())->toBeNull(); // Fetch with size - should have a value - $messagesWithSize = $folder->messages()->withSize()->get(); + $messagesWithSize = $folder->messages()->with(ImapFetchItem::Size)->get(); $message = $messagesWithSize->first(); expect($message->size())->toBeInt(); @@ -188,7 +189,7 @@ function folder(): Folder $uid1 = $folder->messages()->append($shortMessage)->uid(); $uid2 = $folder->messages()->append($longMessage)->uid(); - $messages = $folder->messages()->withSize()->get(); + $messages = $folder->messages()->with(ImapFetchItem::Size)->get(); $short = $messages->find($uid1); $long = $messages->find($uid2); @@ -219,9 +220,11 @@ function folder(): Folder )->uid(); $message = $messages - ->withHeaders() - ->withFlags() - ->withBody() + ->with( + ImapFetchItem::Headers, + ImapFetchItem::Flags, + ImapFetchItem::Body, + ) ->find($uid); expect($message->from()->email())->toBe('foo@email.com'); @@ -249,17 +252,17 @@ function folder(): Folder )->uid(); // Initially, message should not be marked as seen. - $message = $messages->withFlags()->find($uid); + $message = $messages->with(ImapFetchItem::Flags)->find($uid); expect($message->isSeen())->toBeFalse(); // Mark message as seen. $message->markSeen(); - $message = $messages->withFlags()->find($uid); + $message = $messages->with(ImapFetchItem::Flags)->find($uid); expect($message->isSeen())->toBeTrue(); // Unmark message as seen. $message->unmarkSeen(); - $message = $messages->withFlags()->find($uid); + $message = $messages->with(ImapFetchItem::Flags)->find($uid); expect($message->isSeen())->toBeFalse(); }); @@ -275,7 +278,7 @@ function folder(): Folder ) )->uid(); - $message = $messages->withHeaders()->withBody()->find($uid); + $message = $messages->with(ImapFetchItem::Headers, ImapFetchItem::Body)->find($uid); $targetFolder = $folder->mailbox()->folders()->firstOrCreate( $targetFolderName = uniqid() @@ -287,8 +290,7 @@ function folder(): Folder expect($newUid)->toBeGreaterThan(0); $copiedMessage = $targetFolder->messages() - ->withBody() - ->withHeaders() + ->with(ImapFetchItem::Body, ImapFetchItem::Headers) ->findOrFail($newUid); expect($copiedMessage->from()->email())->toBe('foo@email.com'); @@ -307,7 +309,7 @@ function folder(): Folder ) )->uid(); - $message = $messages->withHeaders()->withBody()->find($uid); + $message = $messages->with(ImapFetchItem::Headers, ImapFetchItem::Body)->find($uid); $targetFolder = $folder->mailbox()->folders()->firstOrCreate( $targetFolderName = uniqid() @@ -316,8 +318,7 @@ function folder(): Folder expect($message->move($targetFolderName))->toBeNull(); $targetMessages = $targetFolder->messages() - ->withHeaders() - ->withBody() + ->with(ImapFetchItem::Headers, ImapFetchItem::Body) ->get(); expect($folder->messages()->count())->toBe(0); @@ -344,7 +345,7 @@ function folder(): Folder $message->delete(); - expect($messages->withFlags()->find($uid)->isDeleted())->toBeTrue(); + expect($messages->with(ImapFetchItem::Flags)->find($uid)->isDeleted())->toBeTrue(); }); test('retrieves messages using or statement', function () { @@ -419,10 +420,10 @@ function folder(): Folder $folder->messages() ->markAsRead() - ->withHeaders() + ->with(ImapFetchItem::Headers) ->get(); - $message = $folder->messages()->withFlags()->find($uid); + $message = $folder->messages()->with(ImapFetchItem::Flags)->find($uid); expect($message->isSeen())->toBeTrue(); }); @@ -439,10 +440,10 @@ function folder(): Folder $folder->messages() ->leaveUnread() - ->withHeaders() + ->with(ImapFetchItem::Headers) ->get(); - $message = $folder->messages()->withFlags()->find($uid); + $message = $folder->messages()->with(ImapFetchItem::Flags)->find($uid); expect($message->isSeen())->toBeFalse(); }); @@ -459,7 +460,7 @@ function folder(): Folder expect($folder->messages()->unseen()->count())->toBe(1); - $folder->messages()->withFlags()->find($uid)->markSeen(); + $folder->messages()->with(ImapFetchItem::Flags)->find($uid)->markSeen(); expect($folder->messages()->unseen()->count())->toBe(0); }); diff --git a/tests/Unit/FetchedMessageDataTest.php b/tests/Unit/FetchedMessageDataTest.php new file mode 100644 index 0000000..68d015f --- /dev/null +++ b/tests/Unit/FetchedMessageDataTest.php @@ -0,0 +1,31 @@ +open(); + $stream->feed([ + '* 5 FETCH (UID 42 FLAGS (\\Seen) RFC822.SIZE 1024 BODY[HEADER] "Subject: Test" BODY[TEXT] "Hello world")', + ]); + + $response = (new ImapParser(new ImapTokenizer($stream)))->next(); + + expect($response)->toBeInstanceOf(UntaggedResponse::class); + + $data = FetchedMessageData::fromResponse($response); + $message = $data->toMessage(new Folder(new Mailbox, 'INBOX')); + + expect($data->uid())->toBe(42) + ->and($message->uid())->toBe(42) + ->and($message->flags())->toBe(['\\Seen']) + ->and($message->size())->toBe(1024) + ->and($message->head())->toBe('Subject: Test') + ->and($message->body())->toBe('Hello world'); +}); diff --git a/tests/Unit/ImapFetchItemTest.php b/tests/Unit/ImapFetchItemTest.php new file mode 100644 index 0000000..623ed81 --- /dev/null +++ b/tests/Unit/ImapFetchItemTest.php @@ -0,0 +1,20 @@ +command())->toBe($command); +})->with([ + [ImapFetchItem::Flags, 'FLAGS'], + [ImapFetchItem::Size, 'RFC822.SIZE'], + [ImapFetchItem::Headers, 'BODY.PEEK[HEADER]'], + [ImapFetchItem::Body, 'BODY.PEEK[TEXT]'], + [ImapFetchItem::BodyStructure, 'BODYSTRUCTURE'], +]); + +test('body fetch items can mark messages as read', function (ImapFetchItem $item, string $command) { + expect($item->command(leaveUnread: false))->toBe($command); +})->with([ + [ImapFetchItem::Headers, 'BODY[HEADER]'], + [ImapFetchItem::Body, 'BODY[TEXT]'], +]); diff --git a/tests/Unit/MessageQueryTest.php b/tests/Unit/MessageQueryTest.php index eb048c7..d64e40a 100644 --- a/tests/Unit/MessageQueryTest.php +++ b/tests/Unit/MessageQueryTest.php @@ -3,6 +3,7 @@ use DirectoryTree\ImapEngine\Connection\ImapConnection; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\Connection\Streams\FakeStream; +use DirectoryTree\ImapEngine\Enums\ImapFetchItem; use DirectoryTree\ImapEngine\Enums\ImapFlag; use DirectoryTree\ImapEngine\Enums\ImapSortKey; use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException; @@ -40,6 +41,59 @@ function query(?Mailbox $mailbox = null): MessageQuery expect($query->toImap())->toBe('HEADER MESSAGE-ID "unique-message-id@server.example.com"'); }); +test('fetch items can be added and removed', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1', + 'TAG2 OK UID SEARCH completed', + '* 1 FETCH (UID 1 RFC822.SIZE 1024)', + 'TAG3 OK UID FETCH completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + query($mailbox) + ->with(ImapFetchItem::Flags, ImapFetchItem::Size) + ->without(ImapFetchItem::Flags) + ->get(); + + $stream->assertWritten('TAG3 UID FETCH 1 (RFC822.SIZE)'); +}); + +test('fetch items can be replaced', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1', + 'TAG2 OK UID SEARCH completed', + '* 1 FETCH (UID 1 BODY[HEADER] {0}', + '', + ' BODY[TEXT] {0}', + '', + ')', + 'TAG3 OK UID FETCH completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + query($mailbox) + ->with(ImapFetchItem::Flags) + ->only(ImapFetchItem::Headers, ImapFetchItem::Body) + ->markAsRead() + ->get(); + + $stream->assertWritten('TAG3 UID FETCH 1 (BODY[HEADER] BODY[TEXT])'); +}); + test('destroy', function () { $stream = new FakeStream; $stream->open(); diff --git a/tests/Unit/MessageTest.php b/tests/Unit/MessageTest.php index 0491cb0..14802ac 100644 --- a/tests/Unit/MessageTest.php +++ b/tests/Unit/MessageTest.php @@ -441,7 +441,7 @@ 'password' => 'bar', ]); - // This simulates a message fetched without withBodyStructure(), then accessing text() + // This simulates a message fetched without its body structure, then accessing text(). // The server will respond with: 1) body structure fetch, 2) body part fetch $mailbox->connect(ImapConnection::fake([ '* OK Welcome to IMAP', @@ -458,7 +458,7 @@ $folder = new Folder($mailbox, 'INBOX', [], '/'); - // Message created without body structure data - simulates fetching without withBodyStructure() + // Message created without body structure data. $message = new Message($folder, 1, [], 'From: test@example.com', ''); expect($message->hasBody())->toBeFalse(); From 1bdb7bb0cacae10226e4c22a65a78f6a56054467 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 1 Sep 2026 12:27:25 -0400 Subject: [PATCH 2/2] Model message data as composable fetch items --- src/Enums/ImapFetchItem.php | 26 ------------ src/MessageData.php | 57 +++++++++++++++++++++++++ src/MessageData/Attribute.php | 26 ++++++++++++ src/MessageData/Body.php | 67 ++++++++++++++++++++++++++++++ src/MessageData/FetchItem.php | 16 +++++++ src/MessageQuery.php | 4 +- src/MessageQueryInterface.php | 18 ++------ src/QueriesMessages.php | 39 ++++------------- tests/Integration/MessagesTest.php | 48 ++++++++++----------- tests/Unit/ImapFetchItemTest.php | 20 --------- tests/Unit/MessageDataTest.php | 36 ++++++++++++++++ tests/Unit/MessageQueryTest.php | 11 +++-- 12 files changed, 243 insertions(+), 125 deletions(-) delete mode 100644 src/Enums/ImapFetchItem.php create mode 100644 src/MessageData.php create mode 100644 src/MessageData/Attribute.php create mode 100644 src/MessageData/Body.php create mode 100644 src/MessageData/FetchItem.php delete mode 100644 tests/Unit/ImapFetchItemTest.php create mode 100644 tests/Unit/MessageDataTest.php diff --git a/src/Enums/ImapFetchItem.php b/src/Enums/ImapFetchItem.php deleted file mode 100644 index ed31154..0000000 --- a/src/Enums/ImapFetchItem.php +++ /dev/null @@ -1,26 +0,0 @@ - 'FLAGS', - self::Size => 'RFC822.SIZE', - self::Headers => $leaveUnread ? 'BODY.PEEK[HEADER]' : 'BODY[HEADER]', - self::Body => $leaveUnread ? 'BODY.PEEK[TEXT]' : 'BODY[TEXT]', - self::BodyStructure => 'BODYSTRUCTURE', - }; - } -} diff --git a/src/MessageData.php b/src/MessageData.php new file mode 100644 index 0000000..e5fa7ec --- /dev/null +++ b/src/MessageData.php @@ -0,0 +1,57 @@ +value; + } + + /** + * {@inheritDoc} + */ + public function toImap(): string + { + return $this->value; + } +} diff --git a/src/MessageData/Body.php b/src/MessageData/Body.php new file mode 100644 index 0000000..6fcbf90 --- /dev/null +++ b/src/MessageData/Body.php @@ -0,0 +1,67 @@ +peek = true; + + return $item; + } + + /** + * {@inheritDoc} + */ + public function key(): string + { + return "BODY[{$this->section}]"; + } + + /** + * {@inheritDoc} + */ + public function toImap(): string + { + $item = $this->peek ? 'BODY.PEEK' : 'BODY'; + + return "{$item}[{$this->section}]"; + } +} diff --git a/src/MessageData/FetchItem.php b/src/MessageData/FetchItem.php new file mode 100644 index 0000000..ad1366b --- /dev/null +++ b/src/MessageData/FetchItem.php @@ -0,0 +1,16 @@ +forPage($this->page, $this->limit)->values(); $fetch = array_map( - fn (ImapFetchItem $item) => $item->command($this->fetchAsUnread), + fn (FetchItem $item) => $item->toImap(), $this->fetchItems, ); diff --git a/src/MessageQueryInterface.php b/src/MessageQueryInterface.php index 0aebc0e..21ec393 100644 --- a/src/MessageQueryInterface.php +++ b/src/MessageQueryInterface.php @@ -7,8 +7,8 @@ use DirectoryTree\ImapEngine\Collections\MessageCollection; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; -use DirectoryTree\ImapEngine\Enums\ImapFetchItem; use DirectoryTree\ImapEngine\Enums\ImapSortKey; +use DirectoryTree\ImapEngine\MessageData\FetchItem; use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator; /** @@ -16,16 +16,6 @@ */ interface MessageQueryInterface { - /** - * Don't mark messages as read when fetching. - */ - public function leaveUnread(): MessageQueryInterface; - - /** - * Mark all messages as read when fetching. - */ - public function markAsRead(): MessageQueryInterface; - /** * Set the limit and page for the current query. */ @@ -54,17 +44,17 @@ public function setPage(int $page): MessageQueryInterface; /** * Add items to the message FETCH request. */ - public function with(ImapFetchItem ...$items): static; + public function with(FetchItem ...$items): static; /** * Remove items from the message FETCH request. */ - public function without(ImapFetchItem ...$items): static; + public function without(FetchItem ...$items): static; /** * Replace the items in the message FETCH request. */ - public function only(ImapFetchItem ...$items): static; + public function only(FetchItem ...$items): static; /** * Set the fetch order. diff --git a/src/QueriesMessages.php b/src/QueriesMessages.php index 2a72532..c245a6c 100644 --- a/src/QueriesMessages.php +++ b/src/QueriesMessages.php @@ -3,8 +3,8 @@ namespace DirectoryTree\ImapEngine; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; -use DirectoryTree\ImapEngine\Enums\ImapFetchItem; use DirectoryTree\ImapEngine\Enums\ImapSortKey; +use DirectoryTree\ImapEngine\MessageData\FetchItem; use DirectoryTree\ImapEngine\Support\ForwardsCalls; use Illuminate\Support\Traits\Conditionable; @@ -30,7 +30,7 @@ trait QueriesMessages /** * The items to include in message FETCH requests. * - * @var array + * @var array */ protected array $fetchItems = []; @@ -41,11 +41,6 @@ trait QueriesMessages */ protected string $fetchOrder = 'desc'; - /** - * Whether to leave messages fetched as unread by default. - */ - protected bool $fetchAsUnread = true; - /** * The methods that should be returned from query builder. */ @@ -77,26 +72,6 @@ public function __call(string $method, array $parameters): mixed return $this; } - /** - * {@inheritDoc} - */ - public function leaveUnread(): MessageQueryInterface - { - $this->fetchAsUnread = true; - - return $this; - } - - /** - * {@inheritDoc} - */ - public function markAsRead(): MessageQueryInterface - { - $this->fetchAsUnread = false; - - return $this; - } - /** * {@inheritDoc} */ @@ -150,10 +125,10 @@ public function setPage(int $page): MessageQueryInterface /** * {@inheritDoc} */ - public function with(ImapFetchItem ...$items): static + public function with(FetchItem ...$items): static { foreach ($items as $item) { - $this->fetchItems[$item->value] = $item; + $this->fetchItems[$item->key()] = $item; } return $this; @@ -162,10 +137,10 @@ public function with(ImapFetchItem ...$items): static /** * {@inheritDoc} */ - public function without(ImapFetchItem ...$items): static + public function without(FetchItem ...$items): static { foreach ($items as $item) { - unset($this->fetchItems[$item->value]); + unset($this->fetchItems[$item->key()]); } return $this; @@ -174,7 +149,7 @@ public function without(ImapFetchItem ...$items): static /** * {@inheritDoc} */ - public function only(ImapFetchItem ...$items): static + public function only(FetchItem ...$items): static { $this->fetchItems = []; diff --git a/tests/Integration/MessagesTest.php b/tests/Integration/MessagesTest.php index 03d7416..acc9b9f 100644 --- a/tests/Integration/MessagesTest.php +++ b/tests/Integration/MessagesTest.php @@ -3,9 +3,9 @@ use Carbon\Carbon; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\DraftMessage; -use DirectoryTree\ImapEngine\Enums\ImapFetchItem; use DirectoryTree\ImapEngine\Folder; use DirectoryTree\ImapEngine\Message; +use DirectoryTree\ImapEngine\MessageData; use DirectoryTree\ImapEngine\MessageQuery; use Illuminate\Support\ItemNotFoundException; @@ -142,10 +142,10 @@ function folder(): Folder expect($messages->count())->toBe(1); expect($messages->first()->uid())->toBe($uid); })->with([ - fn (MessageQuery $query) => $query->with(ImapFetchItem::Body), - fn (MessageQuery $query) => $query->with(ImapFetchItem::Flags), - fn (MessageQuery $query) => $query->with(ImapFetchItem::Headers), - fn (MessageQuery $query) => $query->with(ImapFetchItem::Size), + fn (MessageQuery $query) => $query->with(MessageData::text()->peek()), + fn (MessageQuery $query) => $query->with(MessageData::flags()), + fn (MessageQuery $query) => $query->with(MessageData::headers()->peek()), + fn (MessageQuery $query) => $query->with(MessageData::size()), ]); test('get with size', function () { @@ -165,7 +165,7 @@ function folder(): Folder expect($messagesWithoutSize->first()->size())->toBeNull(); // Fetch with size - should have a value - $messagesWithSize = $folder->messages()->with(ImapFetchItem::Size)->get(); + $messagesWithSize = $folder->messages()->with(MessageData::size())->get(); $message = $messagesWithSize->first(); expect($message->size())->toBeInt(); @@ -189,7 +189,7 @@ function folder(): Folder $uid1 = $folder->messages()->append($shortMessage)->uid(); $uid2 = $folder->messages()->append($longMessage)->uid(); - $messages = $folder->messages()->with(ImapFetchItem::Size)->get(); + $messages = $folder->messages()->with(MessageData::size())->get(); $short = $messages->find($uid1); $long = $messages->find($uid2); @@ -221,9 +221,9 @@ function folder(): Folder $message = $messages ->with( - ImapFetchItem::Headers, - ImapFetchItem::Flags, - ImapFetchItem::Body, + MessageData::headers()->peek(), + MessageData::flags(), + MessageData::text()->peek(), ) ->find($uid); @@ -252,17 +252,17 @@ function folder(): Folder )->uid(); // Initially, message should not be marked as seen. - $message = $messages->with(ImapFetchItem::Flags)->find($uid); + $message = $messages->with(MessageData::flags())->find($uid); expect($message->isSeen())->toBeFalse(); // Mark message as seen. $message->markSeen(); - $message = $messages->with(ImapFetchItem::Flags)->find($uid); + $message = $messages->with(MessageData::flags())->find($uid); expect($message->isSeen())->toBeTrue(); // Unmark message as seen. $message->unmarkSeen(); - $message = $messages->with(ImapFetchItem::Flags)->find($uid); + $message = $messages->with(MessageData::flags())->find($uid); expect($message->isSeen())->toBeFalse(); }); @@ -278,7 +278,7 @@ function folder(): Folder ) )->uid(); - $message = $messages->with(ImapFetchItem::Headers, ImapFetchItem::Body)->find($uid); + $message = $messages->with(MessageData::headers()->peek(), MessageData::text()->peek())->find($uid); $targetFolder = $folder->mailbox()->folders()->firstOrCreate( $targetFolderName = uniqid() @@ -290,7 +290,7 @@ function folder(): Folder expect($newUid)->toBeGreaterThan(0); $copiedMessage = $targetFolder->messages() - ->with(ImapFetchItem::Body, ImapFetchItem::Headers) + ->with(MessageData::text()->peek(), MessageData::headers()->peek()) ->findOrFail($newUid); expect($copiedMessage->from()->email())->toBe('foo@email.com'); @@ -309,7 +309,7 @@ function folder(): Folder ) )->uid(); - $message = $messages->with(ImapFetchItem::Headers, ImapFetchItem::Body)->find($uid); + $message = $messages->with(MessageData::headers()->peek(), MessageData::text()->peek())->find($uid); $targetFolder = $folder->mailbox()->folders()->firstOrCreate( $targetFolderName = uniqid() @@ -318,7 +318,7 @@ function folder(): Folder expect($message->move($targetFolderName))->toBeNull(); $targetMessages = $targetFolder->messages() - ->with(ImapFetchItem::Headers, ImapFetchItem::Body) + ->with(MessageData::headers()->peek(), MessageData::text()->peek()) ->get(); expect($folder->messages()->count())->toBe(0); @@ -345,7 +345,7 @@ function folder(): Folder $message->delete(); - expect($messages->with(ImapFetchItem::Flags)->find($uid)->isDeleted())->toBeTrue(); + expect($messages->with(MessageData::flags())->find($uid)->isDeleted())->toBeTrue(); }); test('retrieves messages using or statement', function () { @@ -419,11 +419,10 @@ function folder(): Folder )->uid(); $folder->messages() - ->markAsRead() - ->with(ImapFetchItem::Headers) + ->with(MessageData::headers()) ->get(); - $message = $folder->messages()->with(ImapFetchItem::Flags)->find($uid); + $message = $folder->messages()->with(MessageData::flags())->find($uid); expect($message->isSeen())->toBeTrue(); }); @@ -439,11 +438,10 @@ function folder(): Folder )->uid(); $folder->messages() - ->leaveUnread() - ->with(ImapFetchItem::Headers) + ->with(MessageData::headers()->peek()) ->get(); - $message = $folder->messages()->with(ImapFetchItem::Flags)->find($uid); + $message = $folder->messages()->with(MessageData::flags())->find($uid); expect($message->isSeen())->toBeFalse(); }); @@ -460,7 +458,7 @@ function folder(): Folder expect($folder->messages()->unseen()->count())->toBe(1); - $folder->messages()->with(ImapFetchItem::Flags)->find($uid)->markSeen(); + $folder->messages()->with(MessageData::flags())->find($uid)->markSeen(); expect($folder->messages()->unseen()->count())->toBe(0); }); diff --git a/tests/Unit/ImapFetchItemTest.php b/tests/Unit/ImapFetchItemTest.php deleted file mode 100644 index 623ed81..0000000 --- a/tests/Unit/ImapFetchItemTest.php +++ /dev/null @@ -1,20 +0,0 @@ -command())->toBe($command); -})->with([ - [ImapFetchItem::Flags, 'FLAGS'], - [ImapFetchItem::Size, 'RFC822.SIZE'], - [ImapFetchItem::Headers, 'BODY.PEEK[HEADER]'], - [ImapFetchItem::Body, 'BODY.PEEK[TEXT]'], - [ImapFetchItem::BodyStructure, 'BODYSTRUCTURE'], -]); - -test('body fetch items can mark messages as read', function (ImapFetchItem $item, string $command) { - expect($item->command(leaveUnread: false))->toBe($command); -})->with([ - [ImapFetchItem::Headers, 'BODY[HEADER]'], - [ImapFetchItem::Body, 'BODY[TEXT]'], -]); diff --git a/tests/Unit/MessageDataTest.php b/tests/Unit/MessageDataTest.php new file mode 100644 index 0000000..0536943 --- /dev/null +++ b/tests/Unit/MessageDataTest.php @@ -0,0 +1,36 @@ +key())->toBe($command) + ->and($item->toImap())->toBe($command); +})->with([ + [MessageData::flags(), 'FLAGS'], + [MessageData::size(), 'RFC822.SIZE'], + [MessageData::bodyStructure(), 'BODYSTRUCTURE'], +]); + +test('it creates body section data items', function (FetchItem $item, string $command) { + expect($item->toImap())->toBe($command); +})->with([ + [MessageData::headers(), 'BODY[HEADER]'], + [MessageData::text(), 'BODY[TEXT]'], + [MessageData::section('1.2'), 'BODY[1.2]'], +]); + +test('body section data items can be fetched without setting the seen flag', function (FetchItem $item, string $command) { + expect($item->peek()->toImap())->toBe($command); +})->with([ + [MessageData::headers(), 'BODY.PEEK[HEADER]'], + [MessageData::text(), 'BODY.PEEK[TEXT]'], + [MessageData::section('1.2'), 'BODY.PEEK[1.2]'], +]); + +test('peeking does not modify the original body section data item', function () { + $headers = MessageData::headers(); + + expect($headers->peek())->not->toBe($headers) + ->and($headers->toImap())->toBe('BODY[HEADER]'); +}); diff --git a/tests/Unit/MessageQueryTest.php b/tests/Unit/MessageQueryTest.php index d64e40a..433ac3d 100644 --- a/tests/Unit/MessageQueryTest.php +++ b/tests/Unit/MessageQueryTest.php @@ -3,12 +3,12 @@ use DirectoryTree\ImapEngine\Connection\ImapConnection; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\Connection\Streams\FakeStream; -use DirectoryTree\ImapEngine\Enums\ImapFetchItem; use DirectoryTree\ImapEngine\Enums\ImapFlag; use DirectoryTree\ImapEngine\Enums\ImapSortKey; use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException; use DirectoryTree\ImapEngine\Folder; use DirectoryTree\ImapEngine\Mailbox; +use DirectoryTree\ImapEngine\MessageData; use DirectoryTree\ImapEngine\MessageQuery; function query(?Mailbox $mailbox = null): MessageQuery @@ -58,8 +58,8 @@ function query(?Mailbox $mailbox = null): MessageQuery $mailbox->connect(new ImapConnection($stream)); query($mailbox) - ->with(ImapFetchItem::Flags, ImapFetchItem::Size) - ->without(ImapFetchItem::Flags) + ->with(MessageData::flags(), MessageData::size()) + ->without(MessageData::flags()) ->get(); $stream->assertWritten('TAG3 UID FETCH 1 (RFC822.SIZE)'); @@ -86,9 +86,8 @@ function query(?Mailbox $mailbox = null): MessageQuery $mailbox->connect(new ImapConnection($stream)); query($mailbox) - ->with(ImapFetchItem::Flags) - ->only(ImapFetchItem::Headers, ImapFetchItem::Body) - ->markAsRead() + ->with(MessageData::flags()) + ->only(MessageData::headers(), MessageData::text()) ->get(); $stream->assertWritten('TAG3 UID FETCH 1 (BODY[HEADER] BODY[TEXT])');