Repository: DirectoryTree/ImapEngine (php)

Issue:
New UID on move message to new folder
Hi, when move message to folder, the message get new UID, how do to get that new UID?? the reason are when try to get that message moved into new folder not find because the message has new UID into that folder

Example:
Message into INBOX has UID=40, then move that message to other folder and not have messages, the message has UID=1 into new folder

Pull request description:
Return new message UID when moving or copying message
Closes #55 

Patch:
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 @@ interface ConnectionInterface
      *
      * @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 @@ interface ConnectionInterface
      *
      * @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 @@ class ImapConnection implements ConnectionInterface
     /**
      * {@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 @@ class MessageResponseParser
 
         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 @@ class Message implements Arrayable, JsonSerializable, MessageInterface
     /**
      * 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 @@ class Message implements Arrayable, JsonSerializable, MessageInterface
 
         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(


Test patch:
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 @@ test('copy', function () {
         $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 @@ test('move', function () {
         $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 @@
 <?php
 
 use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
+use DirectoryTree\ImapEngine\Connection\Responses\Data\ResponseCodeData;
 use DirectoryTree\ImapEngine\Connection\Responses\MessageResponseParser;
+use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
 use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
 use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
 use DirectoryTree\ImapEngine\Connection\Tokens\Literal;
@@ -89,3 +91,31 @@ it('handles empty flags list correctly', function () {
 
     expect($parsedFlags)->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\Folder;
 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 @@ test('it moves message using MOVE when capable', function () {
         '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 @@ test('it copies and then deletes message using UIDPLUS when incapable of MOVE',
         '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([