Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions src/Support/Str.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace DirectoryTree\ImapEngine\Support;

use BackedEnum;
use Illuminate\Support\Collection;

class Str
{
Expand Down Expand Up @@ -75,18 +76,12 @@ public static function enum(BackedEnum|string $enum): string
}

/**
* Make a range set for use in a search command.
* Make an IMAP sequence set.
*/
public static function set(int|string|array $from, int|float|string|null $to = null): string
{
// If $from is an array with multiple elements, return them as a comma-separated list.
if (is_array($from) && count($from) > 1) {
return implode(',', $from);
}

// If $from is an array with a single element, return that element.
if (is_array($from) && count($from) === 1) {
return (string) reset($from);
if (is_array($from)) {
return static::toSequenceSet($from);
}

// At this point, $from is an integer. No upper bound provided, return $from as a string.
Expand All @@ -103,6 +98,44 @@ public static function set(int|string|array $from, int|float|string|null $to = n
return $from.':'.$to;
}

/**
* Convert the values into an IMAP sequence set.
*
* @param array<int, int|string> $values
*/
protected static function toSequenceSet(array $values): string
{
return Collection::make(array_values($values))
->chunkWhile(function (int|string $value, int $key, Collection $range) {
$previous = $range->last();

if (! is_numeric($value) || ! is_numeric($previous)) {
return false;
}

$difference = (int) $value - (int) $previous;
$direction = (int) $previous <=> (int) $range->first();

return in_array($difference, [-1, 1], true)
&& ($range->count() === 1 || $difference === $direction);
})
->map(fn (Collection $range) => static::toSequenceRange(
$range->first(),
$range->last(),
))
->implode(',');
}

/**
* Convert the values into an IMAP sequence range.
*/
protected static function toSequenceRange(int|string $start, int|string $end): string
{
return (string) $start === (string) $end
? (string) $start
: $start.':'.$end;
}

/**
* Make a credentials string for use in the AUTHENTICATE command.
*/
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Connection/ImapConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@

$connection->move('Archive', [1, 2, 3]);

$stream->assertWritten('TAG1 UID MOVE 1,2,3 "Archive"');
$stream->assertWritten('TAG1 UID MOVE 1:3 "Archive"');
});

test('store flags', function () {
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Connection/ImapQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ function (ImapQueryBuilder $q) {

$builder->uid([2, 3, 5]);

expect($builder->toImap())->toBe('UID 2,3,5');
expect($builder->toImap())->toBe('UID 2:3,5');
});

test('compiles UID range to infinity with from and to as a numeric upper bound', function () {
Expand Down
20 changes: 10 additions & 10 deletions tests/Unit/MessageQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function query(?Mailbox $mailbox = null): MessageQuery

query($mailbox)->destroy([1, 2, 3]);

$stream->assertWritten('TAG2 UID STORE 1,2,3 +FLAGS.SILENT (\Deleted)');
$stream->assertWritten('TAG2 UID STORE 1:3 +FLAGS.SILENT (\Deleted)');
});

test('oldest sets fetch order to asc', function () {
Expand Down Expand Up @@ -232,7 +232,7 @@ function query(?Mailbox $mailbox = null): MessageQuery
$count = query($mailbox)->flag(ImapFlag::Seen, '+');

expect($count)->toBe(3);
$stream->assertWritten('TAG3 UID STORE 1,2,3 +FLAGS.SILENT (\Seen)');
$stream->assertWritten('TAG3 UID STORE 1:3 +FLAGS.SILENT (\Seen)');
});

test('flag removes flag from all matching messages', function () {
Expand All @@ -253,7 +253,7 @@ function query(?Mailbox $mailbox = null): MessageQuery
$count = query($mailbox)->flag(ImapFlag::Flagged, '-');

expect($count)->toBe(2);
$stream->assertWritten('TAG3 UID STORE 4,5 -FLAGS.SILENT (\Flagged)');
$stream->assertWritten('TAG3 UID STORE 4:5 -FLAGS.SILENT (\Flagged)');
});

test('flag returns zero when no messages match', function () {
Expand Down Expand Up @@ -293,7 +293,7 @@ function query(?Mailbox $mailbox = null): MessageQuery
$count = query($mailbox)->markRead();

expect($count)->toBe(3);
$stream->assertWritten('TAG3 UID STORE 1,2,3 +FLAGS.SILENT (\Seen)');
$stream->assertWritten('TAG3 UID STORE 1:3 +FLAGS.SILENT (\Seen)');
});

test('markUnread marks all matching messages as unread', function () {
Expand All @@ -314,7 +314,7 @@ function query(?Mailbox $mailbox = null): MessageQuery
$count = query($mailbox)->markUnread();

expect($count)->toBe(2);
$stream->assertWritten('TAG3 UID STORE 1,2 -FLAGS.SILENT (\Seen)');
$stream->assertWritten('TAG3 UID STORE 1:2 -FLAGS.SILENT (\Seen)');
});

test('markFlagged flags all matching messages', function () {
Expand All @@ -335,7 +335,7 @@ function query(?Mailbox $mailbox = null): MessageQuery
$count = query($mailbox)->markFlagged();

expect($count)->toBe(4);
$stream->assertWritten('TAG3 UID STORE 5,6,7,8 +FLAGS.SILENT (\Flagged)');
$stream->assertWritten('TAG3 UID STORE 5:8 +FLAGS.SILENT (\Flagged)');
});

test('uid range to infinity searches with numeric upper bound', function () {
Expand Down Expand Up @@ -397,7 +397,7 @@ function query(?Mailbox $mailbox = null): MessageQuery
$count = query($mailbox)->delete();

expect($count)->toBe(3);
$stream->assertWritten('TAG3 UID STORE 1,2,3 +FLAGS.SILENT (\Deleted)');
$stream->assertWritten('TAG3 UID STORE 1:3 +FLAGS.SILENT (\Deleted)');
});

test('delete with expunge also expunges folder', function () {
Expand All @@ -422,7 +422,7 @@ function query(?Mailbox $mailbox = null): MessageQuery
$count = $query->delete(expunge: true);

expect($count)->toBe(2);
$stream->assertWritten('TAG3 UID STORE 1,2 +FLAGS.SILENT (\Deleted)');
$stream->assertWritten('TAG3 UID STORE 1:2 +FLAGS.SILENT (\Deleted)');
$stream->assertWritten('TAG4 EXPUNGE');
});

Expand All @@ -444,7 +444,7 @@ function query(?Mailbox $mailbox = null): MessageQuery
$count = query($mailbox)->move('Archive');

expect($count)->toBe(3);
$stream->assertWritten('TAG3 UID MOVE 1,2,3 "Archive"');
$stream->assertWritten('TAG3 UID MOVE 1:3 "Archive"');
});

test('move returns zero when no messages match', function () {
Expand Down Expand Up @@ -484,7 +484,7 @@ function query(?Mailbox $mailbox = null): MessageQuery
$count = query($mailbox)->copy('Backup');

expect($count)->toBe(2);
$stream->assertWritten('TAG3 UID COPY 4,5 "Backup"');
$stream->assertWritten('TAG3 UID COPY 4:5 "Backup"');
});

test('copy returns zero when no messages match', function () {
Expand Down
12 changes: 11 additions & 1 deletion tests/Unit/Support/StrTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
expect(Str::set(5))->toBe('5');
});

test('set converts consecutive values into sequence ranges', function () {
expect(Str::set([1, 2, 3, 5, 7, 8, 9]))->toBe('1:3,5,7:9');
expect(Str::set([9, 8, 7, 5, 3, 2, 1]))->toBe('9:7,5,3:1');
expect(Str::set(range(16902, 15146)))->toBe('16902:15146');
expect(Str::set([1, 3, 5]))->toBe('1,3,5');
expect(Str::set([1, 2, 3, 8, 7, 6]))->toBe('1:3,8:6');
expect(Str::set(['1', '2', '3', '5']))->toBe('1:3,5');
expect(Str::set([1, '*']))->toBe('1,*');
});

test('credentials', function () {
expect(Str::credentials('foo', 'bar'))->toBe('dXNlcj1mb28BYXV0aD1CZWFyZXIgYmFyAQE=');
});
Expand All @@ -22,7 +32,7 @@
});

test('set ignores $to when $from is a multi-element array', function () {
expect(Str::set([5, 6], 10))->toBe('5,6');
expect(Str::set([5, 6], 10))->toBe('5:6');
});

test('escape removes newlines/control characters and escapes backslashes and double quotes', function () {
Expand Down
Loading