diff --git a/src/Connection/ConnectionInterface.php b/src/Connection/ConnectionInterface.php index 5823c38..2374e1a 100644 --- a/src/Connection/ConnectionInterface.php +++ b/src/Connection/ConnectionInterface.php @@ -244,7 +244,7 @@ public function append(string $folder, string $message, ?array $flags = null): T * * @see https://datatracker.ietf.org/doc/html/rfc9051#name-copy-command */ - public function copy(string $folder, array|int $from, ?int $to = null): void; + public function copy(string $folder, array|int $from, ?int $to = null): TaggedResponse; /** * Send a "UID MOVE" command. @@ -253,7 +253,7 @@ public function copy(string $folder, array|int $from, ?int $to = null): void; * * @see https://datatracker.ietf.org/doc/html/rfc9051#name-move-command */ - public function move(string $folder, array|int $from, ?int $to = null): void; + public function move(string $folder, array|int $from, ?int $to = null): TaggedResponse; /** * Send a "CREATE" command. diff --git a/src/Connection/ImapConnection.php b/src/Connection/ImapConnection.php index 633a71b..d0a40b4 100644 --- a/src/Connection/ImapConnection.php +++ b/src/Connection/ImapConnection.php @@ -353,27 +353,27 @@ public function append(string $folder, string $message, ?array $flags = null): T /** * {@inheritDoc} */ - public function copy(string $folder, array|int $from, ?int $to = null): void + public function copy(string $folder, array|int $from, ?int $to = null): TaggedResponse { $this->send('UID COPY', [ Str::set($from, $to), Str::literal($folder), ], $tag); - $this->assertTaggedResponse($tag); + return $this->assertTaggedResponse($tag); } /** * {@inheritDoc} */ - public function move(string $folder, array|int $from, ?int $to = null): void + public function move(string $folder, array|int $from, ?int $to = null): TaggedResponse { $this->send('UID MOVE', [ Str::set($from, $to), Str::literal($folder), ], $tag); - $this->assertTaggedResponse($tag); + return $this->assertTaggedResponse($tag); } /** diff --git a/src/Connection/Responses/MessageResponseParser.php b/src/Connection/Responses/MessageResponseParser.php index 70f25b2..3e96c5a 100644 --- a/src/Connection/Responses/MessageResponseParser.php +++ b/src/Connection/Responses/MessageResponseParser.php @@ -2,6 +2,8 @@ namespace DirectoryTree\ImapEngine\Connection\Responses; +use DirectoryTree\ImapEngine\Connection\Responses\Data\ResponseCodeData; + class MessageResponseParser { /** @@ -48,4 +50,24 @@ public static function getBodyText(UntaggedResponse $response): array return [$uid => $contents]; } + + /** + * Get the UID from a tagged move or copy response. + */ + public static function getUidFromCopy(TaggedResponse $response): ?int + { + if (! $data = $response->tokenAt(2)) { + return null; + } + + if (! $data instanceof ResponseCodeData) { + return null; + } + + if (! $value = $data->tokenAt(3)?->value) { + return null; + } + + return (int) $value; + } } diff --git a/src/Message.php b/src/Message.php index 3686d31..8461fa9 100644 --- a/src/Message.php +++ b/src/Message.php @@ -3,6 +3,7 @@ namespace DirectoryTree\ImapEngine; use BackedEnum; +use DirectoryTree\ImapEngine\Connection\Responses\MessageResponseParser; use DirectoryTree\ImapEngine\Enums\ImapFlag; use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException; use DirectoryTree\ImapEngine\Support\Str; @@ -281,17 +282,27 @@ public function flag(mixed $flag, string $operation, bool $expunge = false): voi /** * Copy the message to the given folder. */ - public function copy(string $folder): void + public function copy(string $folder): ?int { - $this->folder->mailbox() - ->connection() - ->copy($folder, $this->uid); + $mailbox = $this->folder->mailbox(); + + $capabilities = $mailbox->capabilities(); + + if (! in_array('UIDPLUS', $capabilities)) { + throw new ImapCapabilityException( + 'Unable to copy message. IMAP server does not support UIDPLUS capability' + ); + } + + $response = $mailbox->connection()->copy($folder, $this->uid); + + return MessageResponseParser::getUidFromCopy($response); } /** * Move the message to the given folder. */ - public function move(string $folder, bool $expunge = false): void + public function move(string $folder, bool $expunge = false): ?int { $mailbox = $this->folder->mailbox(); @@ -299,20 +310,20 @@ public function move(string $folder, bool $expunge = false): void switch (true) { case in_array('MOVE', $capabilities): - $mailbox->connection()->move($folder, $this->uid); + $response = $mailbox->connection()->move($folder, $this->uid); if ($expunge) { $this->folder->expunge(); } - return; + return MessageResponseParser::getUidFromCopy($response); case in_array('UIDPLUS', $capabilities): - $this->copy($folder); + $uid = $this->copy($folder); $this->delete($expunge); - return; + return $uid; default: throw new ImapCapabilityException( diff --git a/tests/Integration/MessagesTest.php b/tests/Integration/MessagesTest.php index 8f4a419..e237515 100644 --- a/tests/Integration/MessagesTest.php +++ b/tests/Integration/MessagesTest.php @@ -182,20 +182,18 @@ function folder(): Folder $targetFolderName = uniqid() ); - $message->copy($targetFolderName); + $newUid = $message->copy($targetFolderName); - $targetMessages = $targetFolder->messages() - ->withHeaders() - ->withBody() - ->get(); - - expect($targetMessages->count())->toBe(1); + expect($newUid)->toBeInt(); + expect($newUid)->toBeGreaterThan(0); - /** @var Message $movedMessage */ - $movedMessage = $targetMessages->first(); + $copiedMessage = $targetFolder->messages() + ->withBody() + ->withHeaders() + ->findOrFail($newUid); - expect($movedMessage->from()->email())->toBe('foo@email.com'); - expect($movedMessage->text())->toBe('copy test'); + expect($copiedMessage->from()->email())->toBe('foo@email.com'); + expect($copiedMessage->text())->toBe('copy test'); }); test('move', function () { @@ -216,13 +214,14 @@ function folder(): Folder $targetFolderName = uniqid() ); - $message->move($targetFolderName); + expect($message->move($targetFolderName))->toBeNull(); $targetMessages = $targetFolder->messages() ->withHeaders() ->withBody() ->get(); + expect($folder->messages()->count())->toBe(0); expect($targetMessages->count())->toBe(1); /** @var Message $movedMessage */ diff --git a/tests/Unit/Connection/Responses/MessageResponseParserTest.php b/tests/Unit/Connection/Responses/MessageResponseParserTest.php index bf37c74..8275472 100644 --- a/tests/Unit/Connection/Responses/MessageResponseParserTest.php +++ b/tests/Unit/Connection/Responses/MessageResponseParserTest.php @@ -1,7 +1,9 @@ toBe(['11111' => []]); }); + +it('parses UID from tagged COPYUID response', function () { + $response = new TaggedResponse([ + new Atom('TAG1'), // Tag + new Atom('OK'), // Status + new ResponseCodeData([ + new Atom('COPYUID'), // Response code + new Atom('1570950167'), // UIDVALIDITY + new Atom('1234'), // Source UID + new Atom('5678'), // Destination UID + ]), + new Atom('Move completed.'), // Human-readable text + ]); + + $parsedUid = MessageResponseParser::getUidFromCopy($response); + + expect($parsedUid)->toBe(5678); +}); + +it('returns null for non-COPYUID tagged response', function () { + $response = new TaggedResponse([ + new Atom('TAG1'), + new Atom('OK'), + new Atom('Move completed.'), + ]); + + expect(MessageResponseParser::getUidFromCopy($response))->toBeNull(); +}); diff --git a/tests/Unit/MessageTest.php b/tests/Unit/MessageTest.php index 613945d..6185e6f 100644 --- a/tests/Unit/MessageTest.php +++ b/tests/Unit/MessageTest.php @@ -7,7 +7,7 @@ use DirectoryTree\ImapEngine\Mailbox; use DirectoryTree\ImapEngine\Message; -test('it moves message using MOVE when capable', function () { +test('it moves message using MOVE when capable and returns the new UID', function () { $mailbox = Mailbox::make([ 'username' => 'foo', 'password' => 'bar', @@ -18,17 +18,19 @@ 'TAG1 OK Logged in', '* CAPABILITY IMAP4rev1 STARTTLS MOVE AUTH=PLAIN', 'TAG2 OK CAPABILITY completed', - 'TAG3 OK MOVE completed', + 'TAG3 OK [COPYUID 1234567890 1 42] MOVE completed', ])); $folder = new Folder($mailbox, 'INBOX', [], '/'); $message = new Message($folder, 1, [], 'header', 'body'); - $message->move('INBOX.Sent'); -})->throwsNoExceptions(); + $newUid = $message->move('INBOX.Sent'); + + expect($newUid)->toBe(42); +}); -test('it copies and then deletes message using UIDPLUS when incapable of MOVE', function () { +test('it copies and then deletes message using UIDPLUS when incapable of MOVE and returns the new UID', function () { $mailbox = Mailbox::make([ 'username' => 'foo', 'password' => 'bar', @@ -39,16 +41,18 @@ 'TAG1 OK Logged in', '* CAPABILITY IMAP4rev1 STARTTLS UIDPLUS AUTH=PLAIN', 'TAG2 OK CAPABILITY completed', - 'TAG3 OK UID MOVE completed', - 'TAG4 OK COPY completed', + 'TAG3 OK [COPYUID 1234567890 1 123] COPY completed', + 'TAG4 OK STORE completed', ])); $folder = new Folder($mailbox, 'INBOX', [], '/'); $message = new Message($folder, 1, [], 'header', 'body'); - $message->move('INBOX.Sent'); -})->throwsNoExceptions(); + $newUid = $message->move('INBOX.Sent'); + + expect($newUid)->toBe(123); +}); test('it throws exception when server does not support MOVE or UIDPLUS capabilities', function () { $mailbox = Mailbox::make([