diff --git a/src/MessageQuery.php b/src/MessageQuery.php index c807299..9b0da14 100644 --- a/src/MessageQuery.php +++ b/src/MessageQuery.php @@ -2,6 +2,7 @@ namespace DirectoryTree\ImapEngine; +use BackedEnum; use DirectoryTree\ImapEngine\Collections\MessageCollection; use DirectoryTree\ImapEngine\Collections\ResponseCollection; use DirectoryTree\ImapEngine\Connection\ConnectionInterface; @@ -211,6 +212,106 @@ public function destroy(array|int $uids, bool $expunge = false): void } } + /** + * {@inheritDoc} + */ + public function flag(BackedEnum|string $flag, string $operation, bool $expunge = false): int + { + $uids = $this->search()->all(); + + if (empty($uids)) { + return 0; + } + + $this->connection()->store( + (array) Str::enums($flag), + $uids, + mode: $operation + ); + + if ($expunge) { + $this->folder->expunge(); + } + + return count($uids); + } + + /** + * {@inheritDoc} + */ + public function markRead(): int + { + return $this->flag(ImapFlag::Seen, '+'); + } + + /** + * {@inheritDoc} + */ + public function markUnread(): int + { + return $this->flag(ImapFlag::Seen, '-'); + } + + /** + * {@inheritDoc} + */ + public function markFlagged(): int + { + return $this->flag(ImapFlag::Flagged, '+'); + } + + /** + * {@inheritDoc} + */ + public function unmarkFlagged(): int + { + return $this->flag(ImapFlag::Flagged, '-'); + } + + /** + * {@inheritDoc} + */ + public function delete(bool $expunge = false): int + { + return $this->flag(ImapFlag::Deleted, '+', $expunge); + } + + /** + * {@inheritDoc} + */ + public function move(string $folder, bool $expunge = false): int + { + $uids = $this->search()->all(); + + if (empty($uids)) { + return 0; + } + + $this->connection()->move($folder, $uids); + + if ($expunge) { + $this->folder->expunge(); + } + + return count($uids); + } + + /** + * {@inheritDoc} + */ + public function copy(string $folder): int + { + $uids = $this->search()->all(); + + if (empty($uids)) { + return 0; + } + + $this->connection()->copy($folder, $uids); + + return count($uids); + } + /** * Process the collection of messages. */ diff --git a/src/MessageQueryInterface.php b/src/MessageQueryInterface.php index a2a2d41..7338ea0 100644 --- a/src/MessageQueryInterface.php +++ b/src/MessageQueryInterface.php @@ -2,6 +2,7 @@ namespace DirectoryTree\ImapEngine; +use BackedEnum; use DirectoryTree\ImapEngine\Collections\MessageCollection; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator; @@ -205,4 +206,61 @@ public function find(int $id, ImapFetchIdentifier $identifier = ImapFetchIdentif * Destroy the given messages. */ public function destroy(array|int $uids, bool $expunge = false): void; + + /** + * Add or remove a flag from all messages matching the current query. + * + * @param string $operation '+'|'-' + * @return int The number of messages affected. + */ + public function flag(BackedEnum|string $flag, string $operation, bool $expunge = false): int; + + /** + * Mark all messages matching the current query as read. + * + * @return int The number of messages affected. + */ + public function markRead(): int; + + /** + * Mark all messages matching the current query as unread. + * + * @return int The number of messages affected. + */ + public function markUnread(): int; + + /** + * Mark all messages matching the current query as flagged. + * + * @return int The number of messages affected. + */ + public function markFlagged(): int; + + /** + * Unmark all messages matching the current query as flagged. + * + * @return int The number of messages affected. + */ + public function unmarkFlagged(): int; + + /** + * Delete all messages matching the current query. + * + * @return int The number of messages affected. + */ + public function delete(bool $expunge = false): int; + + /** + * Move all messages matching the current query to the given folder. + * + * @return int The number of messages affected. + */ + public function move(string $folder, bool $expunge = false): int; + + /** + * Copy all messages matching the current query to the given folder. + * + * @return int The number of messages affected. + */ + public function copy(string $folder): int; } diff --git a/src/Testing/FakeMessageQuery.php b/src/Testing/FakeMessageQuery.php index cc0146c..bcabbee 100644 --- a/src/Testing/FakeMessageQuery.php +++ b/src/Testing/FakeMessageQuery.php @@ -2,6 +2,7 @@ namespace DirectoryTree\ImapEngine\Testing; +use BackedEnum; use DirectoryTree\ImapEngine\Collections\MessageCollection; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; @@ -154,4 +155,72 @@ public function destroy(array|int $uids, bool $expunge = false): void $messages->values()->all() ); } + + /** + * {@inheritDoc} + */ + public function flag(BackedEnum|string $flag, string $operation, bool $expunge = false): int + { + return count($this->folder->getMessages()); + } + + /** + * {@inheritDoc} + */ + public function markRead(): int + { + return count($this->folder->getMessages()); + } + + /** + * {@inheritDoc} + */ + public function markUnread(): int + { + return count($this->folder->getMessages()); + } + + /** + * {@inheritDoc} + */ + public function markFlagged(): int + { + return count($this->folder->getMessages()); + } + + /** + * {@inheritDoc} + */ + public function unmarkFlagged(): int + { + return count($this->folder->getMessages()); + } + + /** + * {@inheritDoc} + */ + public function delete(bool $expunge = false): int + { + $count = count($this->folder->getMessages()); + + $this->folder->setMessages([]); + + return $count; + } + + /** + * {@inheritDoc} + */ + public function move(string $folder, bool $expunge = false): int + { + return count($this->folder->getMessages()); + } + + /** + * {@inheritDoc} + */ + public function copy(string $folder): int + { + return count($this->folder->getMessages()); + } } diff --git a/tests/Unit/MessageQueryTest.php b/tests/Unit/MessageQueryTest.php index 517145a..16dc948 100644 --- a/tests/Unit/MessageQueryTest.php +++ b/tests/Unit/MessageQueryTest.php @@ -204,3 +204,275 @@ function query(?Mailbox $mailbox = null): MessageQuery expect($uid)->toBe(1); $stream->assertWritten('TAG2 APPEND "INBOX" (\\Seen) "Hello world"'); })->with([ImapFlag::Seen, '\\Seen']); + +test('flag adds flag to all matching messages', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1 2 3', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID STORE completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->flag(ImapFlag::Seen, '+'); + + expect($count)->toBe(3); + $stream->assertWritten('TAG3 UID STORE 1,2,3 +FLAGS.SILENT (\Seen)'); +}); + +test('flag removes flag from all matching messages', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 4 5', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID STORE completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->flag(ImapFlag::Flagged, '-'); + + expect($count)->toBe(2); + $stream->assertWritten('TAG3 UID STORE 4,5 -FLAGS.SILENT (\Flagged)'); +}); + +test('flag returns zero when no messages match', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH', + 'TAG2 OK SEARCH completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->flag(ImapFlag::Seen, '+'); + + expect($count)->toBe(0); +}); + +test('markRead marks all matching messages as read', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1 2 3', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID STORE completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->markRead(); + + expect($count)->toBe(3); + $stream->assertWritten('TAG3 UID STORE 1,2,3 +FLAGS.SILENT (\Seen)'); +}); + +test('markUnread marks all matching messages as unread', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1 2', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID STORE completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->markUnread(); + + expect($count)->toBe(2); + $stream->assertWritten('TAG3 UID STORE 1,2 -FLAGS.SILENT (\Seen)'); +}); + +test('markFlagged flags all matching messages', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 5 6 7 8', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID STORE completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->markFlagged(); + + expect($count)->toBe(4); + $stream->assertWritten('TAG3 UID STORE 5,6,7,8 +FLAGS.SILENT (\Flagged)'); +}); + +test('unmarkFlagged unflags all matching messages', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 10', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID STORE completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->unmarkFlagged(); + + expect($count)->toBe(1); + $stream->assertWritten('TAG3 UID STORE 10 -FLAGS.SILENT (\Flagged)'); +}); + +test('delete marks all matching messages as deleted', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1 2 3', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID STORE completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->delete(); + + expect($count)->toBe(3); + $stream->assertWritten('TAG3 UID STORE 1,2,3 +FLAGS.SILENT (\Deleted)'); +}); + +test('delete with expunge also expunges folder', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1 2', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID STORE completed', + 'TAG4 OK EXPUNGE completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $folder = new Folder($mailbox, 'INBOX'); + $query = new MessageQuery($folder, new ImapQueryBuilder); + + $count = $query->delete(expunge: true); + + expect($count)->toBe(2); + $stream->assertWritten('TAG3 UID STORE 1,2 +FLAGS.SILENT (\Deleted)'); + $stream->assertWritten('TAG4 EXPUNGE'); +}); + +test('move moves all matching messages to folder', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 1 2 3', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID MOVE completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->move('Archive'); + + expect($count)->toBe(3); + $stream->assertWritten('TAG3 UID MOVE 1,2,3 "Archive"'); +}); + +test('move returns zero when no messages match', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH', + 'TAG2 OK SEARCH completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->move('Archive'); + + expect($count)->toBe(0); +}); + +test('copy copies all matching messages to folder', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 4 5', + 'TAG2 OK SEARCH completed', + 'TAG3 OK UID COPY completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->copy('Backup'); + + expect($count)->toBe(2); + $stream->assertWritten('TAG3 UID COPY 4,5 "Backup"'); +}); + +test('copy returns zero when no messages match', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH', + 'TAG2 OK SEARCH completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $count = query($mailbox)->copy('Backup'); + + expect($count)->toBe(0); +});