Skip to content

Commit 8e37aed

Browse files
authored
Merge pull request #47 from DirectoryTree/task-38
Handle moving messages when server only has UIDPLUS capability
2 parents e2287c0 + e1ebdcf commit 8e37aed

File tree

6 files changed

+161
-31
lines changed

6 files changed

+161
-31
lines changed

src/Folder.php

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@
1313

1414
class Folder implements Arrayable, FolderInterface, JsonSerializable
1515
{
16-
/**
17-
* The folder's cached capabilities.
18-
*/
19-
protected array $capabilities;
20-
2116
/**
2217
* Constructor.
2318
*/
@@ -96,8 +91,8 @@ public function messages(): MessageQuery
9691
*/
9792
public function idle(callable $callback, ?callable $query = null, int $timeout = 300): void
9893
{
99-
if (! $this->hasCapability('IDLE')) {
100-
throw new ImapCapabilityException('IMAP server does not support IDLE');
94+
if (! in_array('IDLE', $this->mailbox->capabilities())) {
95+
throw new ImapCapabilityException('Unable to IDLE. IMAP server does not support IDLE capability.');
10196
}
10297

10398
// The message query to use when fetching messages.
@@ -198,22 +193,6 @@ public function delete(): void
198193
$this->mailbox->connection()->delete($this->path);
199194
}
200195

201-
/**
202-
* Determine if the mailbox has the given capability.
203-
*/
204-
protected function hasCapability(string $capability): bool
205-
{
206-
return in_array($capability, $this->capabilities());
207-
}
208-
209-
/**
210-
* Get and in-memory cache the mailboxes's capabilities.
211-
*/
212-
protected function capabilities(): array
213-
{
214-
return $this->capabilities ??= $this->mailbox->capabilities();
215-
}
216-
217196
/**
218197
* Get the array representation of the folder.
219198
*/

src/Mailbox.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ class Mailbox implements MailboxInterface
3232
],
3333
];
3434

35+
/**
36+
* The cached mailbox capabilities.
37+
*
38+
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.1.1
39+
*/
40+
protected ?array $capabilities = null;
41+
3542
/**
3643
* The currently selected folder.
3744
*/
@@ -194,9 +201,9 @@ public function folders(): FolderRepositoryInterface
194201
*/
195202
public function capabilities(): array
196203
{
197-
return array_map(
204+
return $this->capabilities ??= array_map(
198205
fn (Atom $token) => $token->value,
199-
$this->connection()->capability()->tokensAfter(1)
206+
$this->connection()->capability()->tokensAfter(2)
200207
);
201208
}
202209

src/Message.php

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use BackedEnum;
66
use DirectoryTree\ImapEngine\Enums\ImapFlag;
7+
use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException;
78
use DirectoryTree\ImapEngine\Support\Str;
89
use Illuminate\Contracts\Support\Arrayable;
910
use JsonSerializable;
@@ -292,12 +293,31 @@ public function copy(string $folder): void
292293
*/
293294
public function move(string $folder, bool $expunge = false): void
294295
{
295-
$this->folder->mailbox()
296-
->connection()
297-
->move($folder, $this->uid);
296+
$mailbox = $this->folder->mailbox();
298297

299-
if ($expunge) {
300-
$this->folder->expunge();
298+
$capabilities = $mailbox->capabilities();
299+
300+
switch (true) {
301+
case in_array('MOVE', $capabilities):
302+
$mailbox->connection()->move($folder, $this->uid);
303+
304+
if ($expunge) {
305+
$this->folder->expunge();
306+
}
307+
308+
return;
309+
310+
case in_array('UIDPLUS', $capabilities):
311+
$this->copy($folder);
312+
313+
$this->delete($expunge);
314+
315+
return;
316+
317+
default:
318+
throw new ImapCapabilityException(
319+
'Unable to move message. IMAP server does not support MOVE or UIDPLUS capabilities'
320+
);
301321
}
302322
}
303323

tests/Integration/MailboxTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
$mailbox = mailbox();
1919

2020
expect($mailbox->capabilities())->toBe([
21-
'CAPABILITY',
2221
'IMAP4rev1',
2322
'LITERAL+',
2423
'UIDPLUS',

tests/Unit/MailboxTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,23 @@
119119
expect($folder->path())->toBe('INBOX');
120120
expect($folder->flags())->toBe(['\\HasNoChildren']);
121121
});
122+
123+
test('capabilities', function () {
124+
$mailbox = Mailbox::make([
125+
'username' => 'foo',
126+
'password' => 'bar',
127+
]);
128+
129+
$mailbox->connect(ImapConnection::fake([
130+
'* OK Welcome to IMAP',
131+
'TAG1 OK Logged in',
132+
'* CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN',
133+
'TAG2 OK CAPABILITY completed',
134+
]));
135+
136+
expect($mailbox->capabilities())->toBe([
137+
'IMAP4rev1',
138+
'STARTTLS',
139+
'AUTH=PLAIN',
140+
]);
141+
});

tests/Unit/MessageTest.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
use DirectoryTree\ImapEngine\Connection\ImapConnection;
4+
use DirectoryTree\ImapEngine\Enums\ImapFlag;
5+
use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException;
6+
use DirectoryTree\ImapEngine\Folder;
7+
use DirectoryTree\ImapEngine\Mailbox;
8+
use DirectoryTree\ImapEngine\Message;
9+
10+
test('it moves message using MOVE when capable', function () {
11+
$mailbox = Mailbox::make([
12+
'username' => 'foo',
13+
'password' => 'bar',
14+
]);
15+
16+
$mailbox->connect(ImapConnection::fake([
17+
'* OK Welcome to IMAP',
18+
'TAG1 OK Logged in',
19+
'* CAPABILITY IMAP4rev1 STARTTLS MOVE AUTH=PLAIN',
20+
'TAG2 OK CAPABILITY completed',
21+
'TAG3 OK MOVE completed',
22+
]));
23+
24+
$folder = new Folder($mailbox, 'INBOX', [], '/');
25+
26+
$message = new Message($folder, 1, [], 'header', 'body');
27+
28+
$message->move('INBOX.Sent');
29+
})->throwsNoExceptions();
30+
31+
test('it copies and then deletes message using UIDPLUS when incapable of MOVE', function () {
32+
$mailbox = Mailbox::make([
33+
'username' => 'foo',
34+
'password' => 'bar',
35+
]);
36+
37+
$mailbox->connect(ImapConnection::fake([
38+
'* OK Welcome to IMAP',
39+
'TAG1 OK Logged in',
40+
'* CAPABILITY IMAP4rev1 STARTTLS UIDPLUS AUTH=PLAIN',
41+
'TAG2 OK CAPABILITY completed',
42+
'TAG3 OK UID MOVE completed',
43+
'TAG4 OK COPY completed',
44+
]));
45+
46+
$folder = new Folder($mailbox, 'INBOX', [], '/');
47+
48+
$message = new Message($folder, 1, [], 'header', 'body');
49+
50+
$message->move('INBOX.Sent');
51+
})->throwsNoExceptions();
52+
53+
test('it throws exception when server does not support MOVE or UIDPLUS capabilities', function () {
54+
$mailbox = Mailbox::make([
55+
'username' => 'foo',
56+
'password' => 'bar',
57+
]);
58+
59+
$mailbox->connect(ImapConnection::fake([
60+
'* OK Welcome to IMAP',
61+
'TAG1 OK Logged in',
62+
'* CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN',
63+
'TAG2 OK CAPABILITY completed',
64+
]));
65+
66+
$folder = new Folder($mailbox, 'INBOX', [], '/');
67+
68+
$message = new Message($folder, 1, [], 'header', 'body');
69+
70+
$message->move('INBOX.Sent');
71+
})->throws(ImapCapabilityException::class);
72+
73+
test('it can mark and unmark a message as flagged', function () {
74+
$mailbox = Mailbox::make([
75+
'username' => 'foo',
76+
'password' => 'bar',
77+
]);
78+
79+
$mailbox->connect(ImapConnection::fake([
80+
'* OK Welcome to IMAP',
81+
'TAG1 OK Logged in',
82+
'* CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN',
83+
'TAG2 OK CAPABILITY completed',
84+
'TAG3 OK STORE completed',
85+
]));
86+
87+
$folder = new Folder($mailbox, 'INBOX', [], '/');
88+
89+
$message = new Message($folder, 1, [], 'header', 'body');
90+
91+
expect($message->isFlagged())->toBeFalse();
92+
expect($message->flags())->not->toContain('\\Flagged');
93+
94+
$message->markFlagged();
95+
96+
expect($message->isFlagged())->toBeTrue();
97+
expect($message->flags())->toContain('\\Flagged');
98+
expect($message->hasFlag(ImapFlag::Flagged))->toBeTrue();
99+
100+
$message->unmarkFlagged();
101+
102+
expect($message->isFlagged())->toBeFalse();
103+
expect($message->flags())->not->toContain('\\Flagged');
104+
expect($message->hasFlag(ImapFlag::Flagged))->toBeFalse();
105+
});

0 commit comments

Comments
 (0)