Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4364fad
Add incremental mailbox synchronization and extensible fetched data
stevebauman Sep 2, 2026
61c0b43
Support incremental synchronization through fetch modifiers
stevebauman Sep 2, 2026
95a67d8
Remove redundant connection FETCH shortcuts
stevebauman Sep 2, 2026
e5a2034
Unify IMAP operation identifiers and STORE modifiers
stevebauman Sep 2, 2026
b9f3499
Align connection parameters with IMAP command syntax
stevebauman Sep 2, 2026
67b5640
Send SORT charsets unquoted for server compatibility
stevebauman Sep 2, 2026
b9a7101
Adjust formatting
stevebauman Sep 2, 2026
4fd92fb
Spacing
stevebauman Sep 2, 2026
ba8ce90
Address incremental synchronization review feedback
stevebauman Sep 3, 2026
a4c13d0
Adjust param order
stevebauman Sep 3, 2026
ab63e17
Rename sequence set parser to fromSequenceSet
stevebauman Sep 3, 2026
0844b7b
Harden synchronization results and mailbox reconnects
stevebauman Sep 3, 2026
b5dda15
Extract SASL authentication exchange
stevebauman Sep 5, 2026
68e6ab6
Add fake logger assertions
stevebauman Sep 5, 2026
007487b
Refine capability and selection APIs
stevebauman Sep 5, 2026
cea1083
Fix code style
stevebauman Sep 5, 2026
cba52f6
Make mailbox capabilities immutable snapshots
stevebauman Sep 20, 2026
9494048
Handle selection capability and state edge cases
stevebauman Sep 20, 2026
fdafa28
Improve tokenizer method and enablement interface naming
stevebauman Sep 20, 2026
81822a9
Handle authentication replies within the exchange
stevebauman Sep 20, 2026
5889554
Sort cases
stevebauman Sep 20, 2026
8dcaf34
Adjust comments and spacing
stevebauman Sep 20, 2026
7fc93bf
Construct fake mailboxes through a typed factory
stevebauman Sep 20, 2026
58a3e0e
Validate IMAP arguments and centralize mailbox reset
stevebauman Sep 20, 2026
d396273
Refine FETCH and STORE response handling
stevebauman Sep 20, 2026
3fc9e70
Fix code style
stevebauman Sep 20, 2026
3c3e379
Send atom-safe IMAP charsets without quotes
stevebauman Sep 20, 2026
2636658
Merge branch 'feature/v2-incremental-sync' of https://github.com/Dire…
stevebauman Sep 20, 2026
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
40 changes: 40 additions & 0 deletions src/Authentication/XOAuth2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace DirectoryTree\ImapEngine\Authentication;

use DirectoryTree\ImapEngine\Authenticator;

class XOAuth2 implements Authenticator
{
/**
* Constructor.
*/
public function __construct(
protected string $user,
protected string $token,
) {}

/**
* {@inheritDoc}
*/
public function mechanism(): string
{
return 'XOAUTH2';
}

/**
* {@inheritDoc}
*/
public function initialResponse(): string
{
return "user=$this->user\1auth=Bearer $this->token\1\1";
}

/**
* {@inheritDoc}
*/
public function respond(string $challenge): string
{
return $challenge === '' ? $this->initialResponse() : '';
}
}
21 changes: 21 additions & 0 deletions src/Authenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace DirectoryTree\ImapEngine;

interface Authenticator
{
/**
* Get the SASL mechanism name.
*/
public function mechanism(): string;

/**
* Get the unencoded initial response, or null to await a challenge.
*/
public function initialResponse(): ?string;

/**
* Respond to a decoded challenge. Return null to cancel authentication.
*/
public function respond(string $challenge): ?string;
}
143 changes: 52 additions & 91 deletions src/Connection/ConnectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@

use DateTimeInterface;
use DirectoryTree\ImapEngine\AppendResult;
use DirectoryTree\ImapEngine\Authenticator;
use DirectoryTree\ImapEngine\Collections\ResponseCollection;
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
use DirectoryTree\ImapEngine\Enums\ImapIdentifier;
use DirectoryTree\ImapEngine\FetchModifier;
use DirectoryTree\ImapEngine\FetchResult;
use DirectoryTree\ImapEngine\ImapSort;
use DirectoryTree\ImapEngine\SelectionOption;
use DirectoryTree\ImapEngine\SelectionResult;
use DirectoryTree\ImapEngine\StoreModifier;
use DirectoryTree\ImapEngine\StoreResult;
use Generator;

interface ConnectionInterface
Expand Down Expand Up @@ -51,11 +58,12 @@ public function logout(): void;
/**
* Send an "AUTHENTICATE" command.
*
* Authenticate the current session.
* Authenticate using a SASL mechanism. Initial responses require SASL-IR support.
*
* @see https://datatracker.ietf.org/doc/html/rfc4959
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-authenticate-command
*/
public function authenticate(string $user, string $token): TaggedResponse;
public function authenticate(Authenticator $authenticator, bool $initialResponse = false): TaggedResponse;

/**
* Send a "STARTTLS" command.
Expand All @@ -74,7 +82,7 @@ public function startTls(): void;
public function idle(int $timeout): Generator;

/**
* Send a "DONE" command.
* Send the DONE continuation to finish the current IDLE command.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.13
*/
Expand All @@ -88,13 +96,20 @@ public function done(): void;
public function noop(): TaggedResponse;

/**
* Send a "EXPUNGE" command.
* Send an "ENABLE" command.
*
* Apply session saved changes to the server.
* @see https://datatracker.ietf.org/doc/html/rfc5161
*/
public function enable(string ...$capabilities): ResponseCollection;

/**
* Send an "EXPUNGE" or "UID EXPUNGE" command.
*
* Permanently remove deleted messages, optionally restricted to the given UIDs.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-expunge-command
*/
public function expunge(array|int|null $uids = null): ResponseCollection;
public function expunge(array|int|string|null $uids = null): ResponseCollection;

/**
* Send a "CAPABILITY" command.
Expand All @@ -108,101 +123,43 @@ public function capability(): UntaggedResponse;
/**
* Send a "SEARCH" command.
*
* Execute a search request.
* Execute a search request, returning UIDs by default.
* The charset is omitted by default and must remain omitted after enabling UTF8=ACCEPT.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-search-command
*/
public function search(array $params): UntaggedResponse;
public function search(array $criteria, ImapIdentifier $identifier = ImapIdentifier::Uid, ?string $charset = null): UntaggedResponse;

/**
* Send a "SORT" command.
*
* Execute a sort request using RFC 5256.
* Execute a sort request using RFC 5256, returning UIDs by default.
*
* @see https://datatracker.ietf.org/doc/html/rfc5256
*/
public function sort(ImapSort $sort, array $params): UntaggedResponse;
public function sort(ImapSort $sort, array $criteria, ImapIdentifier $identifier = ImapIdentifier::Uid, string $charset = 'UTF-8'): UntaggedResponse;

/**
* Send a "FETCH" command.
* Send an "ID" command.
*
* Exchange identification information.
*
* @see https://datatracker.ietf.org/doc/html/rfc2971.
*/
public function id(?array $ids = null): UntaggedResponse;

/**
* Send a "FETCH UID" command.
* @param array<string, string|null>|null $parameters
*
* Fetch message UIDs using the given message numbers.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-uid-command
*/
public function uid(int|array $ids, ImapFetchIdentifier $identifier): ResponseCollection;

/**
* Send a "FETCH BODY[TEXT]" command.
*
* Fetch message text contents.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
*/
public function bodyText(int|array $ids, bool $peek = true): ResponseCollection;

/**
* Send a "FETCH BODY[HEADER]" command.
*
* Fetch message headers.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
*/
public function bodyHeader(int|array $ids, bool $peek = true): ResponseCollection;

/**
* Send a "FETCH BODYSTRUCTURE" command.
*
* Fetch message body structure.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
*/
public function bodyStructure(int|array $ids): ResponseCollection;

/**
* Send a "FETCH BODY[i]" command.
*
* Fetch a specific part of the message BODY, such as BODY[1], BODY[1.2], etc.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
*/
public function bodyPart(string $partIndex, int|array $ids, bool $peek = false): ResponseCollection;

/**
* Send a "FETCH FLAGS" command.
*
* Fetch a message flags.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.17
* @see https://datatracker.ietf.org/doc/html/rfc2971.
*/
public function flags(int|array $ids): ResponseCollection;
public function id(?array $parameters = null): UntaggedResponse;

/**
* Send a "FETCH" command.
*
* Fetch one or more items for one or more messages.
* Fetch one or more items or an ALL, FAST, or FULL macro.
* Message sets accept an ID, an array of IDs, or a sequence string such as '1:*'.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-fetch-command
* @see https://datatracker.ietf.org/doc/html/rfc7162#section-3.1.4
*/
public function fetch(array|string $items, array|int $from, mixed $to = null, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): ResponseCollection;

/**
* Send a "RFC822.SIZE" command.
*
* Fetch message sizes for one or more messages.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.21
*/
public function size(int|array $ids): ResponseCollection;
public function fetch(array|int|string $set, array|string $items, ImapIdentifier $identifier = ImapIdentifier::Uid, FetchModifier ...$modifiers): FetchResult;

/**
* Send an IMAP command.
Expand All @@ -216,7 +173,7 @@ public function send(string $name, array $tokens = [], ?string &$tag = null): vo
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-select-command
*/
public function select(string $folder): ResponseCollection;
public function select(string $folder = 'INBOX', SelectionOption ...$options): SelectionResult;

/**
* Send a "EXAMINE" command.
Expand All @@ -225,16 +182,19 @@ public function select(string $folder): ResponseCollection;
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-examine-command
*/
public function examine(string $folder): ResponseCollection;
public function examine(string $folder = 'INBOX', SelectionOption ...$options): SelectionResult;

/**
* Send a "LIST" command.
*
* Get a list of available folders.
* Get folders and any additional responses requested by return options.
* Selection options and multiple patterns require LIST-EXTENDED support.
*
* @see https://datatracker.ietf.org/doc/html/rfc5258
* @see https://datatracker.ietf.org/doc/html/rfc5819
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-list-command
*/
public function list(string $reference = '', string $folder = '*', array $return = []): ResponseCollection;
public function list(string $reference = '', array|string $pattern = '*', array $selection = [], array $return = []): ResponseCollection;

/**
* Send a "STATUS" command.
Expand All @@ -243,16 +203,17 @@ public function list(string $reference = '', string $folder = '*', array $return
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-status-command
*/
public function status(string $folder, array $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): UntaggedResponse;
public function status(string $folder = 'INBOX', array $items = ['MESSAGES', 'UNSEEN', 'UIDNEXT', 'UIDVALIDITY']): UntaggedResponse;

/**
* Send a "STORE" command.
*
* Set message flags.
* Add, remove, or replace message flags using '+', '-', or null as the mode.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-store-command
* @see https://datatracker.ietf.org/doc/html/rfc7162#section-3.1.3
*/
public function store(array|string $flags, array|int $from, ?int $to = null, ?string $mode = null, bool $silent = true, ?string $item = null): ResponseCollection;
public function store(array|int|string $set, array|string $flags, ?string $mode = '+', bool $silent = true, ImapIdentifier $identifier = ImapIdentifier::Uid, StoreModifier ...$modifiers): StoreResult;

/**
* Send a "APPEND" command.
Expand All @@ -264,22 +225,22 @@ public function store(array|string $flags, array|int $from, ?int $to = null, ?st
public function append(string $folder, string $message, ?array $flags = null, ?DateTimeInterface $date = null): AppendResult;

/**
* Send a "UID COPY" command.
* Send a "COPY" or "UID COPY" command.
*
* Copy message set from current folder to other folder.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-copy-command
*/
public function copy(string $folder, array|int $from, ?int $to = null): TaggedResponse;
public function copy(array|int|string $set, string $folder, ImapIdentifier $identifier = ImapIdentifier::Uid): TaggedResponse;

/**
* Send a "UID MOVE" command.
* Send a "MOVE" or "UID MOVE" command.
*
* Move a message set from current folder to another folder.
*
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-move-command
*/
public function move(string $folder, array|int $from, ?int $to = null): TaggedResponse;
public function move(array|int|string $set, string $folder, ImapIdentifier $identifier = ImapIdentifier::Uid): TaggedResponse;

/**
* Send a "CREATE" command.
Expand Down Expand Up @@ -333,7 +294,7 @@ public function unsubscribe(string $folder): TaggedResponse;
*
* @see https://datatracker.ietf.org/doc/html/rfc9208#name-getquota
*/
public function quota(string $root): UntaggedResponse;
public function getQuota(string $root): UntaggedResponse;

/**
* Send a "GETQUOTAROOT" command.
Expand All @@ -342,5 +303,5 @@ public function quota(string $root): UntaggedResponse;
*
* @see https://datatracker.ietf.org/doc/html/rfc9208#name-getquotaroot
*/
public function quotaRoot(string $mailbox): ResponseCollection;
public function getQuotaRoot(string $mailbox): ResponseCollection;
}
Loading
Loading