diff --git a/src/Folder.php b/src/Folder.php index d587527..341fc66 100644 --- a/src/Folder.php +++ b/src/Folder.php @@ -13,11 +13,6 @@ class Folder implements Arrayable, FolderInterface, JsonSerializable { - /** - * The folder's cached capabilities. - */ - protected array $capabilities; - /** * Constructor. */ @@ -96,8 +91,8 @@ public function messages(): MessageQuery */ public function idle(callable $callback, ?callable $query = null, int $timeout = 300): void { - if (! $this->hasCapability('IDLE')) { - throw new ImapCapabilityException('IMAP server does not support IDLE'); + if (! in_array('IDLE', $this->mailbox->capabilities())) { + throw new ImapCapabilityException('Unable to IDLE. IMAP server does not support IDLE capability.'); } // The message query to use when fetching messages. @@ -198,22 +193,6 @@ public function delete(): void $this->mailbox->connection()->delete($this->path); } - /** - * Determine if the mailbox has the given capability. - */ - protected function hasCapability(string $capability): bool - { - return in_array($capability, $this->capabilities()); - } - - /** - * Get and in-memory cache the mailboxes's capabilities. - */ - protected function capabilities(): array - { - return $this->capabilities ??= $this->mailbox->capabilities(); - } - /** * Get the array representation of the folder. */ diff --git a/src/Mailbox.php b/src/Mailbox.php index 8cc2a68..5adc0ba 100644 --- a/src/Mailbox.php +++ b/src/Mailbox.php @@ -32,6 +32,13 @@ class Mailbox implements MailboxInterface ], ]; + /** + * The cached mailbox capabilities. + * + * @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.1.1 + */ + protected ?array $capabilities = null; + /** * The currently selected folder. */ @@ -194,9 +201,9 @@ public function folders(): FolderRepositoryInterface */ public function capabilities(): array { - return array_map( + return $this->capabilities ??= array_map( fn (Atom $token) => $token->value, - $this->connection()->capability()->tokensAfter(1) + $this->connection()->capability()->tokensAfter(2) ); } diff --git a/src/Message.php b/src/Message.php index 3e34c6d..3686d31 100644 --- a/src/Message.php +++ b/src/Message.php @@ -4,6 +4,7 @@ use BackedEnum; use DirectoryTree\ImapEngine\Enums\ImapFlag; +use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException; use DirectoryTree\ImapEngine\Support\Str; use Illuminate\Contracts\Support\Arrayable; use JsonSerializable; @@ -292,12 +293,31 @@ public function copy(string $folder): void */ public function move(string $folder, bool $expunge = false): void { - $this->folder->mailbox() - ->connection() - ->move($folder, $this->uid); + $mailbox = $this->folder->mailbox(); - if ($expunge) { - $this->folder->expunge(); + $capabilities = $mailbox->capabilities(); + + switch (true) { + case in_array('MOVE', $capabilities): + $mailbox->connection()->move($folder, $this->uid); + + if ($expunge) { + $this->folder->expunge(); + } + + return; + + case in_array('UIDPLUS', $capabilities): + $this->copy($folder); + + $this->delete($expunge); + + return; + + default: + throw new ImapCapabilityException( + 'Unable to move message. IMAP server does not support MOVE or UIDPLUS capabilities' + ); } } diff --git a/tests/Integration/MailboxTest.php b/tests/Integration/MailboxTest.php index c6938b9..a6cc9b2 100644 --- a/tests/Integration/MailboxTest.php +++ b/tests/Integration/MailboxTest.php @@ -18,7 +18,6 @@ $mailbox = mailbox(); expect($mailbox->capabilities())->toBe([ - 'CAPABILITY', 'IMAP4rev1', 'LITERAL+', 'UIDPLUS', diff --git a/tests/Unit/MailboxTest.php b/tests/Unit/MailboxTest.php index dcc2dbf..ecd5fd3 100644 --- a/tests/Unit/MailboxTest.php +++ b/tests/Unit/MailboxTest.php @@ -119,3 +119,23 @@ expect($folder->path())->toBe('INBOX'); expect($folder->flags())->toBe(['\\HasNoChildren']); }); + +test('capabilities', function () { + $mailbox = Mailbox::make([ + 'username' => 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN', + 'TAG2 OK CAPABILITY completed', + ])); + + expect($mailbox->capabilities())->toBe([ + 'IMAP4rev1', + 'STARTTLS', + 'AUTH=PLAIN', + ]); +}); diff --git a/tests/Unit/MessageTest.php b/tests/Unit/MessageTest.php new file mode 100644 index 0000000..613945d --- /dev/null +++ b/tests/Unit/MessageTest.php @@ -0,0 +1,105 @@ + 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* CAPABILITY IMAP4rev1 STARTTLS MOVE AUTH=PLAIN', + 'TAG2 OK CAPABILITY completed', + 'TAG3 OK MOVE completed', + ])); + + $folder = new Folder($mailbox, 'INBOX', [], '/'); + + $message = new Message($folder, 1, [], 'header', 'body'); + + $message->move('INBOX.Sent'); +})->throwsNoExceptions(); + +test('it copies and then deletes message using UIDPLUS when incapable of MOVE', function () { + $mailbox = Mailbox::make([ + 'username' => 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* CAPABILITY IMAP4rev1 STARTTLS UIDPLUS AUTH=PLAIN', + 'TAG2 OK CAPABILITY completed', + 'TAG3 OK UID MOVE completed', + 'TAG4 OK COPY completed', + ])); + + $folder = new Folder($mailbox, 'INBOX', [], '/'); + + $message = new Message($folder, 1, [], 'header', 'body'); + + $message->move('INBOX.Sent'); +})->throwsNoExceptions(); + +test('it throws exception when server does not support MOVE or UIDPLUS capabilities', function () { + $mailbox = Mailbox::make([ + 'username' => 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN', + 'TAG2 OK CAPABILITY completed', + ])); + + $folder = new Folder($mailbox, 'INBOX', [], '/'); + + $message = new Message($folder, 1, [], 'header', 'body'); + + $message->move('INBOX.Sent'); +})->throws(ImapCapabilityException::class); + +test('it can mark and unmark a message as flagged', function () { + $mailbox = Mailbox::make([ + 'username' => 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN', + 'TAG2 OK CAPABILITY completed', + 'TAG3 OK STORE completed', + ])); + + $folder = new Folder($mailbox, 'INBOX', [], '/'); + + $message = new Message($folder, 1, [], 'header', 'body'); + + expect($message->isFlagged())->toBeFalse(); + expect($message->flags())->not->toContain('\\Flagged'); + + $message->markFlagged(); + + expect($message->isFlagged())->toBeTrue(); + expect($message->flags())->toContain('\\Flagged'); + expect($message->hasFlag(ImapFlag::Flagged))->toBeTrue(); + + $message->unmarkFlagged(); + + expect($message->isFlagged())->toBeFalse(); + expect($message->flags())->not->toContain('\\Flagged'); + expect($message->hasFlag(ImapFlag::Flagged))->toBeFalse(); +});