diff --git a/src/Connection/ConnectionInterface.php b/src/Connection/ConnectionInterface.php index 93f111b..b1cade2 100644 --- a/src/Connection/ConnectionInterface.php +++ b/src/Connection/ConnectionInterface.php @@ -313,4 +313,22 @@ public function subscribe(string $folder): TaggedResponse; * @see https://datatracker.ietf.org/doc/html/rfc9051#name-unsubscribe-command */ public function unsubscribe(string $folder): TaggedResponse; + + /** + * Send a "GETQUOTA" command. + * + * Retrieve quota information about a specific quota root. + * + * @see https://datatracker.ietf.org/doc/html/rfc9208#name-getquota + */ + public function quota(string $root): UntaggedResponse; + + /** + * Send a "GETQUOTAROOT" command. + * + * Retrieve quota root information about a mailbox. + * + * @see https://datatracker.ietf.org/doc/html/rfc9208#name-getquotaroot + */ + public function quotaRoot(string $mailbox): ResponseCollection; } diff --git a/src/Connection/ImapConnection.php b/src/Connection/ImapConnection.php index e41484d..80c4f9e 100644 --- a/src/Connection/ImapConnection.php +++ b/src/Connection/ImapConnection.php @@ -314,6 +314,34 @@ public function unsubscribe(string $folder): TaggedResponse return $this->assertTaggedResponse($tag); } + /** + * {@inheritDoc} + */ + public function quota(string $root): UntaggedResponse + { + $this->send('GETQUOTA', [Str::literal($root)], tag: $tag); + + $this->assertTaggedResponse($tag); + + return $this->result->responses()->untagged()->firstOrFail( + fn (UntaggedResponse $response) => $response->type()->is('QUOTA') + ); + } + + /** + * {@inheritDoc} + */ + public function quotaRoot(string $mailbox): ResponseCollection + { + $this->send('GETQUOTAROOT', [Str::literal($mailbox)], tag: $tag); + + $this->assertTaggedResponse($tag); + + return $this->result->responses()->untagged()->filter( + fn (UntaggedResponse $response) => $response->type()->is('QUOTA') + ); + } + /** * {@inheritDoc} */ diff --git a/src/Folder.php b/src/Folder.php index 1b72e66..01c0d85 100644 --- a/src/Folder.php +++ b/src/Folder.php @@ -155,6 +155,37 @@ public function select(bool $force = false): void $this->mailbox->select($this, $force); } + /** + * {@inheritDoc} + */ + public function quota(): array + { + if (! in_array('QUOTA', $this->mailbox->capabilities())) { + throw new ImapCapabilityException( + 'Unable to fetch mailbox quotas. IMAP server does not support QUOTA capability.' + ); + } + + $responses = $this->mailbox->connection()->quotaRoot($this->path); + + $values = []; + + foreach ($responses as $response) { + $resource = $response->tokenAt(2); + + $tokens = $response->tokenAt(3)->tokens(); + + for ($i = 0; $i + 2 < count($tokens); $i += 3) { + $values[$resource->value][$tokens[$i]->value] = [ + 'usage' => (int) $tokens[$i + 1]->value, + 'limit' => (int) $tokens[$i + 2]->value, + ]; + } + } + + return $values; + } + /** * {@inheritDoc} */ diff --git a/src/FolderInterface.php b/src/FolderInterface.php index e0d1102..aa89a2c 100644 --- a/src/FolderInterface.php +++ b/src/FolderInterface.php @@ -56,6 +56,11 @@ public function move(string $newPath): void; */ public function select(bool $force = false): void; + /** + * Get the folder's quotas. + */ + public function quota(): array; + /** * Get the folder's status. */ diff --git a/src/Testing/FakeFolder.php b/src/Testing/FakeFolder.php index d2c6116..cb1f3a7 100644 --- a/src/Testing/FakeFolder.php +++ b/src/Testing/FakeFolder.php @@ -136,6 +136,25 @@ public function expunge(): array return []; } + /** + * {@inheritDoc} + */ + public function quota(): array + { + return [ + $this->path => [ + 'STORAGE' => [ + 'usage' => 0, + 'limit' => 0, + ], + 'MESSAGE' => [ + 'usage' => 0, + 'limit' => 0, + ], + ], + ]; + } + /** * {@inheritDoc} */ diff --git a/tests/Integration/FoldersTest.php b/tests/Integration/FoldersTest.php index ff06149..8d86527 100644 --- a/tests/Integration/FoldersTest.php +++ b/tests/Integration/FoldersTest.php @@ -102,3 +102,9 @@ expect($mailbox->folders()->find('foo'))->toBeNull(); }); + +test('quota', function () { + $folder = mailbox()->inbox(); + + expect($folder->quota())->toBeArray(); +}); diff --git a/tests/Unit/FolderTest.php b/tests/Unit/FolderTest.php index 81552bc..ebdf8f1 100644 --- a/tests/Unit/FolderTest.php +++ b/tests/Unit/FolderTest.php @@ -1,5 +1,7 @@ name())->toBe($mixedUtf8FolderName); }); + +test('it returns quota data for the mailbox', function () { + $mailbox = Mailbox::make([ + 'username' => 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* LIST (\\HasNoChildren) "/" "INBOX"', + 'TAG2 OK LIST completed', + '* CAPABILITY IMAP4rev1 LITERAL+ UIDPLUS SORT IDLE MOVE QUOTA', + 'TAG3 OK CAPABILITY completed', + '* QUOTA "INBOX" (STORAGE 54 512)', + '* QUOTA "INBOX" (MESSAGE 12 1024)', + 'TAG4 OK GETQUOTAROOT completed', + ])); + + expect($mailbox->inbox()->quota()) + ->toBeArray() + ->toMatchArray([ + 'INBOX' => [ + 'STORAGE' => [ + 'usage' => 54, + 'limit' => 512, + ], + 'MESSAGE' => [ + 'usage' => 12, + 'limit' => 1024, + ], + ], + ]); +}); + +test('it returns quota data for the mailbox when there are no quotas', function () { + $mailbox = Mailbox::make([ + 'username' => 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* LIST (\\HasNoChildren) "/" "INBOX"', + 'TAG2 OK LIST completed', + '* CAPABILITY IMAP4rev1 LITERAL+ UIDPLUS SORT IDLE MOVE QUOTA', + 'TAG3 OK CAPABILITY completed', + 'TAG4 OK GETQUOTAROOT completed', + ])); + + expect($mailbox->inbox()->quota())->toBe([]); +}); + +test('it returns quota data for the mailbox when there are multiple resources', function () { + $mailbox = Mailbox::make([ + 'username' => 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* LIST (\\HasNoChildren) "/" "INBOX"', + 'TAG2 OK LIST completed', + '* CAPABILITY IMAP4rev1 LITERAL+ UIDPLUS SORT IDLE MOVE QUOTA', + 'TAG3 OK CAPABILITY completed', + '* QUOTA "FOO" (STORAGE 54 512)', + '* QUOTA "FOO" (MESSAGE 12 1024)', + '* QUOTA "BAR" (STORAGE 10 1024)', + '* QUOTA "BAR" (MESSAGE 5 1024)', + 'TAG4 OK GETQUOTAROOT completed', + ])); + + expect($mailbox->inbox()->quota()) + ->toBeArray() + ->toMatchArray([ + 'FOO' => [ + 'STORAGE' => [ + 'usage' => 54, + 'limit' => 512, + ], + 'MESSAGE' => [ + 'usage' => 12, + 'limit' => 1024, + ], + ], + 'BAR' => [ + 'STORAGE' => [ + 'usage' => 10, + 'limit' => 1024, + ], + 'MESSAGE' => [ + 'usage' => 5, + 'limit' => 1024, + ], + ], + ]); +}); + +test('it returns quota data for the mailbox when there are multiple resources in the same list data', function () { + $mailbox = Mailbox::make([ + 'username' => 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* LIST (\\HasNoChildren) "/" "INBOX"', + 'TAG2 OK LIST completed', + '* CAPABILITY IMAP4rev1 LITERAL+ UIDPLUS SORT IDLE MOVE QUOTA', + 'TAG3 OK CAPABILITY completed', + '* QUOTA "FOO" (STORAGE 54 512 MESSAGE 12 1024)', + '* QUOTA "BAR" (STORAGE 10 1024 MESSAGE 5 1024)', + 'TAG4 OK GETQUOTAROOT completed', + ])); + + expect($mailbox->inbox()->quota()) + ->toBeArray() + ->toMatchArray([ + 'FOO' => [ + 'STORAGE' => [ + 'usage' => 54, + 'limit' => 512, + ], + 'MESSAGE' => [ + 'usage' => 12, + 'limit' => 1024, + ], + ], + 'BAR' => [ + 'STORAGE' => [ + 'usage' => 10, + 'limit' => 1024, + ], + 'MESSAGE' => [ + 'usage' => 5, + 'limit' => 1024, + ], + ], + ]); +}); + +test('it throws an imap capability exception when inspecting quotas when the imap server does not support quotas', function () { + $mailbox = Mailbox::make([ + 'username' => 'foo', + 'password' => 'bar', + ]); + + $mailbox->connect(ImapConnection::fake([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* LIST (\\HasNoChildren) "/" "INBOX"', + 'TAG2 OK LIST completed', + '* CAPABILITY IMAP4rev1 LITERAL+ UIDPLUS SORT IDLE MOVE', + 'TAG3 OK CAPABILITY completed', + ])); + + $mailbox->inbox()->quota(); +})->throws(ImapCapabilityException::class); diff --git a/tests/Unit/Testing/FakeFolderTest.php b/tests/Unit/Testing/FakeFolderTest.php index d3db4a9..0a1d1e9 100644 --- a/tests/Unit/Testing/FakeFolderTest.php +++ b/tests/Unit/Testing/FakeFolderTest.php @@ -111,3 +111,20 @@ expect($folder->messages()->where('Unseen')->count())->toBe(3); expect($folder->messages()->where('Seen')->count())->toBe(3); }); + +test('it returns stub quota values', function () { + $folder = new FakeFolder('INBOX'); + + expect($folder->quota())->toBe([ + 'INBOX' => [ + 'STORAGE' => [ + 'usage' => 0, + 'limit' => 0, + ], + 'MESSAGE' => [ + 'usage' => 0, + 'limit' => 0, + ], + ], + ]); +});