Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
39 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
d5bbe4a
Sort match asc
stevebauman Sep 21, 2026
c4e730d
Potential fix for pull request finding 'Require 64-bit PHP for 63-bit…
stevebauman Sep 21, 2026
2e34fd6
Potential fix for pull request finding 'Normalize case-insensitive VA…
stevebauman Sep 21, 2026
cf94985
Simplify fetched response handling
stevebauman Sep 21, 2026
1c03e60
Fix code style
stevebauman Sep 21, 2026
35ea32d
Adjust bool prop name
stevebauman Sep 21, 2026
d8cf4b4
Validate IMAP command tokens
stevebauman Sep 21, 2026
91d49c3
Potential fix for pull request finding 'Validate LIST selection optio…
stevebauman Sep 22, 2026
25b493b
Mirror selection capability checks in fake mailbox
stevebauman Sep 22, 2026
9702316
Track examined folders in fake mailbox
stevebauman Sep 22, 2026
089f1dd
Preserve fetched message identifiers
stevebauman Sep 22, 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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
}
],
"require": {
"php": "^8.1",
"php": "^8.2",
Comment thread
Copilot marked this conversation as resolved.
"symfony/mime": ">=6.0",
"nesbot/carbon": ">=2.0",
"illuminate/collections": ">=9.0",
Expand Down
51 changes: 51 additions & 0 deletions src/Authentication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace DirectoryTree\ImapEngine;

use DirectoryTree\ImapEngine\Connection\ConnectionInterface;
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
use Throwable;

class Authentication
{
/**
* Constructor.
*/
public function __construct(
protected ConnectionInterface $connection,
protected AuthenticatorInterface $authenticator,
) {}

/**
* Authenticate the connection.
*/
public function authenticate(bool $initial = false): TaggedResponse
{
$response = $this->authenticator->initial();
$sent = $initial && $response !== null;

$exchange = $this->connection->authenticate(
$this->authenticator->mechanism(),
$sent ? $response : null,
);

foreach ($exchange as $challenge) {
try {
if (! $sent && $response !== null) {
$answer = $response;
$sent = true;
} else {
$answer = $this->authenticator->respond($challenge);
}
} catch (Throwable $e) {
$this->connection->disconnect();

throw $e;
}

$this->connection->respond($answer);
}

return $exchange->getReturn();
}
}
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\AuthenticatorInterface;

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

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

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

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

namespace DirectoryTree\ImapEngine;

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

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

/**
* Respond to a decoded challenge. Return null to cancel authentication.
*/
public function respond(string $challenge): ?string;
}
93 changes: 93 additions & 0 deletions src/Capabilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace DirectoryTree\ImapEngine;

readonly class Capabilities
{
/**
* The mailbox capabilities.
*
* @var array<string, Capability>
*/
protected array $items;

/**
* Constructor.
*
* @param array<string, Capability> $items
*/
protected function __construct(array $items)
{
$this->items = $items;
}

/**
* Create a capability collection from the given items.
*/
public static function from(Capability ...$capabilities): static
{
$items = [];

foreach ($capabilities as $capability) {
$items[$capability->name()] = $capability;
}

return new static($items);
}

/**
* Get the capability items.
*
* @return array<string, Capability>
*/
public function items(): array
{
return $this->items;
}

/**
* Get all supported capabilities.
*/
public function all(): array
{
return array_keys($this->items);
}

/**
* Determine if the exact capability exists.
*/
public function has(string $capability): bool
{
return isset($this->items[strtoupper($capability)]);
}

/**
* Determine if the capability is supported.
*/
public function supports(string $capability): bool
{
return (bool) $this->find($capability);
}

/**
* Determine if the capability is enabled.
*/
public function enabled(string $capability): bool
{
return ($this->items[strtoupper($capability)] ?? null)?->enabled() ?? false;
}

/**
* Find a supported capability.
*/
protected function find(string $capability): ?Capability
{
foreach ($this->items as $item) {
if ($item->matches($capability)) {
return $item;
}
}

return null;
}
}
49 changes: 49 additions & 0 deletions src/Capability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace DirectoryTree\ImapEngine;

readonly class Capability
{
/**
* Constructor.
*/
protected function __construct(
protected string $name,
protected bool $enabled = false,
) {}

/**
* Make a new capability instance.
*/
public static function make(string $name, bool $enabled = false): static
{
return new static(strtoupper($name), $enabled);
}

/**
* Get the capability name.
*/
public function name(): string
{
return $this->name;
}

/**
* Determine if the capability matches the given name.
*/
public function matches(string $capability): bool
{
$capability = strtoupper($capability);

return $this->name === $capability
|| str_starts_with($this->name, "{$capability}=");
}

/**
* Determine if the capability is enabled.
*/
public function enabled(): bool
{
return $this->enabled;
}
}
Loading
Loading