generated from Nodarx/template
refactor: use custom imap client
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\DateTime;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\QuotedString;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\SynchronizingLiteral;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\ParenthesizedList;
|
||||
|
||||
final readonly class AppendCommand extends Command implements Continuable
|
||||
{
|
||||
/**
|
||||
* @param list<string>|null $flags
|
||||
*/
|
||||
public function __construct(
|
||||
string $mailboxName,
|
||||
private string $message,
|
||||
?array $flags,
|
||||
?DateTimeInterface $internalDate
|
||||
) {
|
||||
parent::__construct(
|
||||
'APPEND',
|
||||
...array_filter([
|
||||
new QuotedString($mailboxName),
|
||||
ParenthesizedList::tryFrom($flags),
|
||||
DateTime::tryFrom($internalDate),
|
||||
new SynchronizingLiteral($this->message),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public function continue(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument;
|
||||
|
||||
interface Argument
|
||||
{
|
||||
public function __toString(): string;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
readonly class Date implements Argument
|
||||
{
|
||||
public function __construct(private DateTimeInterface $value)
|
||||
{
|
||||
}
|
||||
|
||||
public static function tryFrom(?DateTimeInterface $value): ?self
|
||||
{
|
||||
return is_null($value) ? null : new self($value);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value->format('d-M-Y');
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
readonly class DateTime implements Argument
|
||||
{
|
||||
public function __construct(private DateTimeInterface $value)
|
||||
{
|
||||
}
|
||||
|
||||
public static function tryFrom(?DateTimeInterface $value): ?self
|
||||
{
|
||||
return is_null($value) ? null : new self($value);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return '"'.$this->value->format('d-M-Y H:i:s O').'"';
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument;
|
||||
|
||||
final readonly class ParenthesizedList implements Argument
|
||||
{
|
||||
/**
|
||||
* @param list<string> $items
|
||||
*/
|
||||
public function __construct(public array $items)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $items
|
||||
*/
|
||||
public static function tryFrom(?array $items): ?self
|
||||
{
|
||||
return empty($items) ? null : new self($items);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('(%s)', implode(' ', $this->items));
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument;
|
||||
|
||||
final readonly class QuotedString implements Argument
|
||||
{
|
||||
public function __construct(private string $value)
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('"%s"', $this->value);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
class All implements Criteria
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'ALL';
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Date;
|
||||
|
||||
readonly class Before extends Date implements Criteria
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'BEFORE '.parent::__toString();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class Body implements Criteria
|
||||
{
|
||||
public function __construct(private string $value) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'BODY "' . str_replace(['"', '\\'], ['\\"', '\\\\'], $this->value) . '"';
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Argument;
|
||||
|
||||
interface Criteria extends Argument
|
||||
{
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class Flagged implements Criteria
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'FLAGGED';
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class From implements Criteria
|
||||
{
|
||||
public function __construct(private string $value) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'FROM "' . str_replace(['"', '\\'], ['\\"', '\\\\'], $this->value) . '"';
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\QuotedString;
|
||||
|
||||
class Header implements Criteria
|
||||
{
|
||||
public function __construct(
|
||||
private string $fieldName,
|
||||
private string $value,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('HEADER %s %s', $this->fieldName, new QuotedString($this->value));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class Larger implements Criteria
|
||||
{
|
||||
public function __construct(private int $size) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'LARGER ' . $this->size;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class Not implements Criteria
|
||||
{
|
||||
public function __construct(private Criteria $criteria)
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'NOT ('.$this->criteria.')';
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class Seen implements Criteria
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'SEEN';
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Date;
|
||||
|
||||
readonly class Since extends Date implements Criteria
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'SINCE '.parent::__toString();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class Smaller implements Criteria
|
||||
{
|
||||
public function __construct(private int $size) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'SMALLER ' . $this->size;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class Subject implements Criteria
|
||||
{
|
||||
public function __construct(private string $value) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'SUBJECT "' . str_replace(['"', '\\'], ['\\"', '\\\\'], $this->value) . '"';
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class To implements Criteria
|
||||
{
|
||||
public function __construct(private string $value) {}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'TO "' . str_replace(['"', '\\'], ['\\"', '\\\\'], $this->value) . '"';
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class Unflagged implements Criteria
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'UNFLAGGED';
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||
|
||||
final readonly class Unseen implements Criteria
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'UNSEEN';
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument;
|
||||
|
||||
final class SequenceSet implements Argument
|
||||
{
|
||||
/**
|
||||
* @var array<int>
|
||||
*/
|
||||
private array $numbers;
|
||||
private ?string $range;
|
||||
|
||||
public function __construct(int ...$numbers)
|
||||
{
|
||||
$this->numbers = $numbers;
|
||||
$this->range = null;
|
||||
}
|
||||
|
||||
public static function range(int $from, int $to): self
|
||||
{
|
||||
$set = new self();
|
||||
$set->range = $from . ':' . $to;
|
||||
return $set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a SequenceSet that matches every message in the mailbox (1:*).
|
||||
*/
|
||||
public static function all(): self
|
||||
{
|
||||
$set = new self();
|
||||
$set->range = '1:*';
|
||||
return $set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a SequenceSet from a flat array of UIDs, collapsing consecutive
|
||||
* values into n:m ranges.
|
||||
*
|
||||
* Examples:
|
||||
* [1, 2, 3, 5, 6, 10] → "1:3,5:6,10"
|
||||
* [42] → "42"
|
||||
* [7, 3, 4, 5] → "3:5,7"
|
||||
*
|
||||
* @param int[] $uids
|
||||
*/
|
||||
public static function list(array $uids): self
|
||||
{
|
||||
if (empty($uids)) {
|
||||
return new self();
|
||||
}
|
||||
|
||||
$uids = array_unique($uids);
|
||||
sort($uids);
|
||||
|
||||
$ranges = [];
|
||||
$start = $end = $uids[0];
|
||||
|
||||
for ($i = 1, $count = count($uids); $i <= $count; $i++) {
|
||||
$current = $uids[$i] ?? null;
|
||||
if ($current !== null && $current === $end + 1) {
|
||||
$end = $current;
|
||||
} else {
|
||||
$ranges[] = $start === $end ? (string) $start : $start . ':' . $end;
|
||||
if ($current !== null) {
|
||||
$start = $end = $current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$set = new self();
|
||||
$set->range = implode(',', $ranges);
|
||||
return $set;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->range !== null) {
|
||||
return $this->range;
|
||||
}
|
||||
|
||||
return implode(',', $this->numbers);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument\Store;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Argument;
|
||||
|
||||
final readonly class Flags implements Argument
|
||||
{
|
||||
/**
|
||||
* @param list<string> $flags
|
||||
*/
|
||||
public function __construct(
|
||||
private array $flags,
|
||||
private string $modifier = '',
|
||||
private bool $silent = true,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%sFLAGS%s (%s)',
|
||||
$this->modifier,
|
||||
$this->silent ? '.SILENT' : '',
|
||||
implode(' ', $this->flags),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Argument;
|
||||
|
||||
final readonly class SynchronizingLiteral implements Argument
|
||||
{
|
||||
public function __construct(private string $value)
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'{%s}',
|
||||
strlen($this->value)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Authenticate;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Argument;
|
||||
use Gricob\IMAP\Protocol\Command\Continuable;
|
||||
|
||||
interface SASLMechanism extends Argument, Continuable
|
||||
{
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command\Authenticate;
|
||||
|
||||
final readonly class XOAuth2 implements SASLMechanism
|
||||
{
|
||||
public function __construct(
|
||||
private string $user,
|
||||
private string $accessToken
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'XOAUTH2';
|
||||
}
|
||||
|
||||
public function continue(): string
|
||||
{
|
||||
return base64_encode(
|
||||
sprintf("user=%s\1auth=Bearer %s\1\1", $this->user, $this->accessToken)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Authenticate\SASLMechanism;
|
||||
|
||||
readonly class AuthenticateCommand extends Command implements Continuable
|
||||
{
|
||||
public function __construct(private SASLMechanism $mechanism)
|
||||
{
|
||||
parent::__construct('AUTHENTICATE', $mechanism);
|
||||
}
|
||||
|
||||
public function continue(): string
|
||||
{
|
||||
return $this->mechanism->continue();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Argument;
|
||||
use Stringable;
|
||||
|
||||
abstract readonly class Command implements Stringable
|
||||
{
|
||||
private string $command;
|
||||
|
||||
/**
|
||||
* @var Argument[]
|
||||
*/
|
||||
private array $arguments;
|
||||
|
||||
public function __construct(
|
||||
string $command,
|
||||
Argument ...$arguments,
|
||||
) {
|
||||
$this->command = $command;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
public function command(): string
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s %s',
|
||||
$this->command,
|
||||
implode(' ', $this->arguments)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
interface Continuable
|
||||
{
|
||||
public function continue(): string;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\QuotedString;
|
||||
|
||||
final readonly class CreateCommand extends Command
|
||||
{
|
||||
public function __construct(string $mailboxName)
|
||||
{
|
||||
parent::__construct('CREATE', new QuotedString($mailboxName));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
final readonly class ExpungeCommand extends Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('EXPUNGE');
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\ParenthesizedList;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\SequenceSet;
|
||||
|
||||
final readonly class FetchCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @param bool $uid
|
||||
* @param SequenceSet $sequenceSet
|
||||
* @param list<string> $items
|
||||
*/
|
||||
public function __construct(
|
||||
bool $uid,
|
||||
SequenceSet $sequenceSet,
|
||||
array $items,
|
||||
) {
|
||||
parent::__construct(
|
||||
$uid ? 'UID FETCH' : 'FETCH',
|
||||
$sequenceSet,
|
||||
new ParenthesizedList($items),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\QuotedString;
|
||||
|
||||
readonly class ListCommand extends Command
|
||||
{
|
||||
public function __construct(string $referenceName, string $pattern)
|
||||
{
|
||||
parent::__construct(
|
||||
'LIST',
|
||||
new QuotedString($referenceName),
|
||||
new QuotedString($pattern)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\QuotedString;
|
||||
|
||||
final readonly class LogInCommand extends Command
|
||||
{
|
||||
public function __construct(string $user, string $password)
|
||||
{
|
||||
parent::__construct(
|
||||
'LOGIN',
|
||||
new QuotedString($user),
|
||||
new QuotedString($password)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Criteria;
|
||||
|
||||
final readonly class SearchCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
bool $uid,
|
||||
Criteria ...$criteria,
|
||||
) {
|
||||
parent::__construct(
|
||||
$uid ? 'UID SEARCH' : 'SEARCH',
|
||||
...$criteria,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\QuotedString;
|
||||
|
||||
readonly class SelectCommand extends Command
|
||||
{
|
||||
public function __construct(string $mailbox)
|
||||
{
|
||||
parent::__construct('SELECT', new QuotedString($mailbox));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
/**
|
||||
* STARTTLS command (RFC 3501 §6.2.1) — patched into gricob/imap.
|
||||
*
|
||||
* After the server responds OK, upgradeTls() must be called on the underlying
|
||||
* SocketConnection to complete the TLS handshake.
|
||||
*/
|
||||
final readonly class StartTlsCommand extends Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('STARTTLS');
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Command;
|
||||
|
||||
use Gricob\IMAP\Protocol\Command\Argument\SequenceSet;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Store\Flags;
|
||||
|
||||
final readonly class StoreCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
bool $uid,
|
||||
SequenceSet $sequenceSet,
|
||||
Flags $dataItem
|
||||
) {
|
||||
parent::__construct(
|
||||
$uid ? 'UID STORE' : 'STORE',
|
||||
$sequenceSet,
|
||||
$dataItem,
|
||||
);
|
||||
}
|
||||
}
|
||||
86
lib/Client/Protocol/CommandExecutor.php
Normal file
86
lib/Client/Protocol/CommandExecutor.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol;
|
||||
|
||||
use Generator;
|
||||
use KTXM\ProviderImap\Client\Command\CommandInterface;
|
||||
use KTXM\ProviderImap\Client\ImapException;
|
||||
use KTXM\ProviderImap\Client\Protocol\Response\TaggedResponse;
|
||||
use KTXM\ProviderImap\Client\Protocol\Response\UntaggedResponse;
|
||||
use KTXM\ProviderImap\Client\SessionContext;
|
||||
use KTXM\ProviderImap\Client\SessionState;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class CommandExecutor
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProtocolReader $reader,
|
||||
private readonly ProtocolWriter $writer,
|
||||
private readonly TagGenerator $tags = new TagGenerator(),
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @template TResult
|
||||
* @param CommandInterface<TResult> $command
|
||||
* @return TResult
|
||||
*/
|
||||
public function perform(CommandInterface $command, SessionContext $context): mixed
|
||||
{
|
||||
$this->assertState($command->allowedStates(), $context->state(), $command->name());
|
||||
|
||||
$this->logger?->debug('IMAP command execution started: {command} (state={state})', [
|
||||
'command' => $command->name(),
|
||||
'state' => $context->state()->value,
|
||||
]);
|
||||
|
||||
$tag = $this->tags->next();
|
||||
$frame = $command->encode($tag, $context);
|
||||
$this->writer->write($tag, $frame);
|
||||
|
||||
return $command->handle(new ResponseStream(function () use ($tag, $context): Generator {
|
||||
yield from $this->responsesUntilCompletion($tag, $context);
|
||||
}), $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<SessionState> $allowedStates
|
||||
*/
|
||||
private function assertState(array $allowedStates, SessionState $currentState, string $commandName): void
|
||||
{
|
||||
foreach ($allowedStates as $allowedState) {
|
||||
if ($allowedState === $currentState) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ImapException(sprintf(
|
||||
'Command %s is not allowed while session is in state %s.',
|
||||
$commandName,
|
||||
$currentState->value,
|
||||
));
|
||||
}
|
||||
|
||||
private function responsesUntilCompletion(string $tag, SessionContext $context): Generator
|
||||
{
|
||||
while (true) {
|
||||
$response = $this->reader->readResponse();
|
||||
|
||||
if ($response instanceof UntaggedResponse && $response->label() === 'CAPABILITY') {
|
||||
$context->replaceCapabilities(...$response->payloadTokens());
|
||||
}
|
||||
|
||||
yield $response;
|
||||
|
||||
if ($response instanceof TaggedResponse && $response->tag() === $tag) {
|
||||
$this->logger?->debug('IMAP command execution completed: tag={tag} status={status}', [
|
||||
'tag' => $tag,
|
||||
'status' => $response->status(),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol;
|
||||
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
||||
use RuntimeException;
|
||||
|
||||
class CommandFailed extends RuntimeException
|
||||
{
|
||||
public static function withStatus(Status $status): self
|
||||
{
|
||||
return new self($status->message);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol;
|
||||
|
||||
use Generator;
|
||||
use Gricob\IMAP\Protocol\Command\Command;
|
||||
use Gricob\IMAP\Protocol\Command\Continuable;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Line;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
||||
use Gricob\IMAP\Protocol\Response\Response;
|
||||
use Gricob\IMAP\Transport\Connection;
|
||||
use RuntimeException;
|
||||
|
||||
final readonly class CommandInteraction implements ContinuationHandler
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private ResponseHandler $responseHandler,
|
||||
private string $tag,
|
||||
private Command $command,
|
||||
) {
|
||||
}
|
||||
|
||||
public function interact(): Response
|
||||
{
|
||||
$request = sprintf(
|
||||
"%s %s\r\n",
|
||||
$this->tag,
|
||||
$this->command,
|
||||
);
|
||||
|
||||
$this->connection->send($request);
|
||||
$streamResponse = $this->connection->receive();
|
||||
|
||||
return $this->responseHandler->handle($this->tag, $streamResponse, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like interact() but yields each untagged Line immediately as it arrives.
|
||||
* The terminal Status is the generator's return value.
|
||||
*
|
||||
* @return Generator<int, Line, mixed, Status>
|
||||
*/
|
||||
public function streamInteract(): Generator
|
||||
{
|
||||
$request = sprintf(
|
||||
"%s %s\r\n",
|
||||
$this->tag,
|
||||
$this->command,
|
||||
);
|
||||
|
||||
$this->connection->send($request);
|
||||
$streamResponse = $this->connection->receive();
|
||||
|
||||
yield from $this->responseHandler->stream($this->tag, $streamResponse, $this);
|
||||
}
|
||||
|
||||
public function continue(): void
|
||||
{
|
||||
if (!$this->command instanceof Continuable) {
|
||||
throw new RuntimeException(
|
||||
sprintf('Command %s does not support continuable interaction', $this->command->command())
|
||||
);
|
||||
}
|
||||
|
||||
$this->connection->send($this->command->continue()."\r\n");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class ConnectionRejected extends RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol;
|
||||
|
||||
interface ContinuationHandler
|
||||
{
|
||||
public function continue(): void;
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol;
|
||||
|
||||
use Generator;
|
||||
use Gricob\IMAP\Protocol\Command\Command;
|
||||
use Gricob\IMAP\Protocol\Command\StartTlsCommand;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Line;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\StatusType;
|
||||
use Gricob\IMAP\Protocol\Response\Parser\Parser;
|
||||
use Gricob\IMAP\Protocol\Response\Response;
|
||||
use Gricob\IMAP\Transport\Connection;
|
||||
use RuntimeException;
|
||||
|
||||
class Imap
|
||||
{
|
||||
protected Connection $connection;
|
||||
private TagGenerator $tagGenerator;
|
||||
private ResponseHandler $responseHandler;
|
||||
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->tagGenerator = new TagGenerator();
|
||||
$this->responseHandler = new ResponseHandler(new Parser());
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->disconnect();
|
||||
}
|
||||
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->connection->isOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connection->open();
|
||||
|
||||
$responseStream = $this->connection->receive();
|
||||
|
||||
$greeting = $this->responseHandler->handle('*', $responseStream, new UnexpectedContinuationHandler());
|
||||
|
||||
match ($greeting->status->type) {
|
||||
StatusType::OK => null, // Do nothing
|
||||
StatusType::PREAUTH => throw new RuntimeException('pre-auth is not supported'),
|
||||
StatusType::BAD,
|
||||
StatusType::NO,
|
||||
StatusType::BYE => throw new ConnectionRejected($greeting->status->message),
|
||||
};
|
||||
}
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
$this->connection->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform STARTTLS negotiation (patch).
|
||||
*
|
||||
* Sends the STARTTLS command and upgrades the underlying socket to TLS.
|
||||
* The connection must be a SocketConnection (or any Connection that
|
||||
* implements upgradeTls()). Call this after connect() but before logIn().
|
||||
*
|
||||
* @throws \RuntimeException if the server rejects STARTTLS
|
||||
* @throws \BadMethodCallException if the connection does not support TLS upgrade
|
||||
*/
|
||||
public function startTls(): void
|
||||
{
|
||||
if (!method_exists($this->connection, 'upgradeTls')) {
|
||||
throw new \BadMethodCallException(
|
||||
'The current Connection implementation does not support STARTTLS upgrade'
|
||||
);
|
||||
}
|
||||
|
||||
$response = $this->send(new StartTlsCommand());
|
||||
|
||||
if ($response->status->type !== StatusType::OK) {
|
||||
throw new \RuntimeException(
|
||||
'Server rejected STARTTLS: ' . $response->status->message
|
||||
);
|
||||
}
|
||||
|
||||
$this->connection->upgradeTls();
|
||||
}
|
||||
|
||||
public function send(Command $command): Response
|
||||
{
|
||||
$interaction = new CommandInteraction(
|
||||
$this->connection,
|
||||
$this->responseHandler,
|
||||
$this->tagGenerator->next(),
|
||||
$command,
|
||||
);
|
||||
|
||||
$response = $interaction->interact();
|
||||
|
||||
if ($response->status->type != StatusType::OK) {
|
||||
throw CommandFailed::withStatus($response->status);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends $command and returns a Generator that yields each untagged Line as
|
||||
* it arrives from the socket. CommandFailed is thrown (inside the generator)
|
||||
* if the server responds with NO or BAD.
|
||||
*
|
||||
* @return Generator<int, Line, mixed, Status>
|
||||
*/
|
||||
public function sendStreaming(Command $command): Generator
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$interaction = new CommandInteraction(
|
||||
$this->connection,
|
||||
$this->responseHandler,
|
||||
$this->tagGenerator->next(),
|
||||
$command,
|
||||
);
|
||||
|
||||
yield from $interaction->streamInteract();
|
||||
}
|
||||
}
|
||||
121
lib/Client/Protocol/ProtocolReader.php
Normal file
121
lib/Client/Protocol/ProtocolReader.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol;
|
||||
|
||||
use KTXM\ProviderImap\Client\ImapException;
|
||||
use KTXM\ProviderImap\Client\Protocol\Response\ContinuationResponse;
|
||||
use KTXM\ProviderImap\Client\Protocol\Response\GreetingResponse;
|
||||
use KTXM\ProviderImap\Client\Protocol\Response\ResponseInterface;
|
||||
use KTXM\ProviderImap\Client\Protocol\Response\TaggedResponse;
|
||||
use KTXM\ProviderImap\Client\Protocol\Response\UntaggedResponse;
|
||||
use KTXM\ProviderImap\Client\Transport\ConnectionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class ProtocolReader
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConnectionInterface $connection,
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
public function readGreeting(): GreetingResponse
|
||||
{
|
||||
$raw = $this->trimTrailingLineEnding($this->connection->readLine());
|
||||
|
||||
if (!str_starts_with($raw, '* ')) {
|
||||
throw new ImapException(sprintf('Expected IMAP greeting, got: %s', $raw));
|
||||
}
|
||||
|
||||
$parts = preg_split('/\s+/', substr($raw, 2), 2) ?: [];
|
||||
$status = strtoupper($parts[0] ?? '');
|
||||
$text = $parts[1] ?? '';
|
||||
|
||||
$this->logger?->debug('IMAP greeting received: {raw}', [
|
||||
'status' => $status,
|
||||
'raw' => $raw,
|
||||
]);
|
||||
|
||||
return new GreetingResponse($status, $text, $raw);
|
||||
}
|
||||
|
||||
public function readResponse(): ResponseInterface
|
||||
{
|
||||
$raw = $this->readRawResponse();
|
||||
|
||||
if ($raw === '') {
|
||||
throw new ImapException('Received empty IMAP response line.');
|
||||
}
|
||||
|
||||
if (str_starts_with($raw, '* ')) {
|
||||
$parts = preg_split('/\s+/', substr($raw, 2), 2) ?: [];
|
||||
$label = strtoupper($parts[0] ?? '');
|
||||
$this->logger?->debug('IMAP untagged response received: {raw}', [
|
||||
'label' => $label,
|
||||
'raw' => $raw,
|
||||
]);
|
||||
return new UntaggedResponse(
|
||||
$label,
|
||||
$parts[1] ?? '',
|
||||
$raw,
|
||||
);
|
||||
}
|
||||
|
||||
if (str_starts_with($raw, '+')) {
|
||||
$this->logger?->debug('IMAP continuation response received: {raw}', [
|
||||
'raw' => $raw,
|
||||
]);
|
||||
return new ContinuationResponse(ltrim(substr($raw, 1)), $raw);
|
||||
}
|
||||
|
||||
$parts = preg_split('/\s+/', $raw, 3) ?: [];
|
||||
|
||||
if (count($parts) < 2) {
|
||||
throw new ImapException(sprintf('Malformed tagged IMAP response: %s', $raw));
|
||||
}
|
||||
|
||||
$status = strtoupper($parts[1]);
|
||||
$this->logger?->debug('IMAP tagged response received: {raw}', [
|
||||
'tag' => $parts[0],
|
||||
'status' => $status,
|
||||
'raw' => $raw,
|
||||
]);
|
||||
|
||||
return new TaggedResponse($parts[0], $status, $parts[2] ?? '', $raw);
|
||||
}
|
||||
|
||||
private function readRawResponse(): string
|
||||
{
|
||||
$raw = $this->connection->readLine();
|
||||
|
||||
while (($literalLength = $this->trailingLiteralLength($raw)) !== null) {
|
||||
$raw .= $this->connection->readBytes($literalLength);
|
||||
$raw .= $this->connection->readLine();
|
||||
}
|
||||
|
||||
return $this->trimTrailingLineEnding($raw);
|
||||
}
|
||||
|
||||
private function trailingLiteralLength(string $raw): ?int
|
||||
{
|
||||
if (preg_match('/\{(\d+)\}\r?\n$/', $raw, $matches) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
private function trimTrailingLineEnding(string $raw): string
|
||||
{
|
||||
if (str_ends_with($raw, "\r\n")) {
|
||||
return substr($raw, 0, -2);
|
||||
}
|
||||
|
||||
if (str_ends_with($raw, "\n")) {
|
||||
return substr($raw, 0, -1);
|
||||
}
|
||||
|
||||
return $raw;
|
||||
}
|
||||
}
|
||||
44
lib/Client/Protocol/ProtocolWriter.php
Normal file
44
lib/Client/Protocol/ProtocolWriter.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol;
|
||||
|
||||
use KTXM\ProviderImap\Client\Transport\ConnectionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class ProtocolWriter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConnectionInterface $connection,
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
public function write(string $tag, RequestFrame $frame): void
|
||||
{
|
||||
$wire = $frame->toWire($tag);
|
||||
|
||||
$this->logger?->debug('IMAP command sent: {raw}', [
|
||||
'tag' => $tag,
|
||||
'command' => strtok($frame->commandLine(), ' ') ?: $frame->commandLine(),
|
||||
'raw' => $this->sanitizeWire($wire),
|
||||
]);
|
||||
|
||||
$this->connection->write($wire);
|
||||
}
|
||||
|
||||
private function sanitizeWire(string $wire): string
|
||||
{
|
||||
$trimmed = rtrim($wire, "\r\n");
|
||||
|
||||
if (preg_match('/^(\S+\s+LOGIN\s+".*?"\s+)".*"$/i', $trimmed, $matches)) {
|
||||
return $matches[1] . '"[REDACTED]"';
|
||||
}
|
||||
|
||||
if (preg_match('/^(\S+\s+AUTHENTICATE\s+\S+)(?:\s+.+)?$/i', $trimmed, $matches)) {
|
||||
return $matches[1] . ' [REDACTED]';
|
||||
}
|
||||
|
||||
return $trimmed;
|
||||
}
|
||||
}
|
||||
22
lib/Client/Protocol/RequestFrame.php
Normal file
22
lib/Client/Protocol/RequestFrame.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol;
|
||||
|
||||
final class RequestFrame
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $commandLine,
|
||||
) {}
|
||||
|
||||
public function commandLine(): string
|
||||
{
|
||||
return $this->commandLine;
|
||||
}
|
||||
|
||||
public function toWire(string $tag): string
|
||||
{
|
||||
return $tag . ' ' . $this->commandLine . "\r\n";
|
||||
}
|
||||
}
|
||||
23
lib/Client/Protocol/Response/ContinuationResponse.php
Normal file
23
lib/Client/Protocol/Response/ContinuationResponse.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol\Response;
|
||||
|
||||
final class ContinuationResponse implements ResponseInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $text,
|
||||
private readonly string $raw,
|
||||
) {}
|
||||
|
||||
public function text(): string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
public function raw(): string
|
||||
{
|
||||
return $this->raw;
|
||||
}
|
||||
}
|
||||
29
lib/Client/Protocol/Response/GreetingResponse.php
Normal file
29
lib/Client/Protocol/Response/GreetingResponse.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol\Response;
|
||||
|
||||
final class GreetingResponse implements ResponseInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $status,
|
||||
private readonly string $text,
|
||||
private readonly string $raw,
|
||||
) {}
|
||||
|
||||
public function status(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function text(): string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
public function raw(): string
|
||||
{
|
||||
return $this->raw;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line;
|
||||
|
||||
final readonly class CommandContinuation implements Line
|
||||
{
|
||||
public function __construct(
|
||||
public string $message,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||
|
||||
final readonly class CapabilityData implements Data
|
||||
{
|
||||
/**
|
||||
* @param list<string> $capabilities
|
||||
*/
|
||||
public function __construct(public array $capabilities)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||
|
||||
use Gricob\IMAP\Protocol\Response\Line\Line;
|
||||
|
||||
interface Data extends Line
|
||||
{
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||
|
||||
final readonly class ExistsData implements Data
|
||||
{
|
||||
public function __construct(public int $numberOfMessages)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||
|
||||
final readonly class ExpungeData implements Data
|
||||
{
|
||||
public function __construct(public int $id)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch;
|
||||
|
||||
final readonly class Address
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $displayName,
|
||||
public ?string $atDomainList,
|
||||
public ?string $mailboxName,
|
||||
public ?string $hostName,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch;
|
||||
|
||||
final readonly class BodySection
|
||||
{
|
||||
public function __construct(public string $section, public string $text)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch;
|
||||
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure\Part;
|
||||
|
||||
class BodyStructure
|
||||
{
|
||||
public function __construct(
|
||||
public Part $part,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
|
||||
final readonly class Disposition
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function __construct(
|
||||
public string $type,
|
||||
public array $attributes,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\Envelope;
|
||||
|
||||
readonly class MessagePart extends SinglePart
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
* @param string[]|null $language
|
||||
*/
|
||||
public function __construct(
|
||||
array $attributes,
|
||||
?string $id,
|
||||
?string $description,
|
||||
string $encoding,
|
||||
int $size,
|
||||
public Envelope $envelope,
|
||||
public BodyStructure $bodyStructure,
|
||||
public int $textLines,
|
||||
?string $md5,
|
||||
?Disposition $disposition,
|
||||
?array $language,
|
||||
?string $location,
|
||||
) {
|
||||
parent::__construct(
|
||||
'MESSAGE',
|
||||
'RFC822',
|
||||
$attributes,
|
||||
$id,
|
||||
$description,
|
||||
$encoding,
|
||||
$size,
|
||||
$md5,
|
||||
$disposition,
|
||||
$language,
|
||||
$location,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
|
||||
final readonly class MultiPart extends Part
|
||||
{
|
||||
/**
|
||||
* @param array<string,string> $attributes
|
||||
* @param string[] $language
|
||||
* @param list<Part> $parts
|
||||
*/
|
||||
public function __construct(
|
||||
string $subtype,
|
||||
array $attributes,
|
||||
public array $parts,
|
||||
public ?Disposition $disposition,
|
||||
public ?array $language,
|
||||
public ?string $location,
|
||||
) {
|
||||
parent::__construct('MULTIPART', $subtype, $attributes);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
|
||||
abstract readonly class Part
|
||||
{
|
||||
/**
|
||||
* @param array<string,string> $attributes
|
||||
*/
|
||||
public function __construct(
|
||||
public string $type,
|
||||
public string $subtype,
|
||||
public array $attributes,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
|
||||
readonly class SinglePart extends Part
|
||||
{
|
||||
/**
|
||||
* @param array<string,string> $attributes
|
||||
* @param string[]|null $language
|
||||
*/
|
||||
public function __construct(
|
||||
string $type,
|
||||
string $subtype,
|
||||
array $attributes,
|
||||
public ?string $id,
|
||||
public ?string $description,
|
||||
public string $encoding,
|
||||
public int $size,
|
||||
public ?string $md5,
|
||||
public ?Disposition $disposition,
|
||||
public ?array $language,
|
||||
public ?string $location,
|
||||
) {
|
||||
parent::__construct($type, $subtype, $attributes);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
|
||||
final readonly class TextPart extends SinglePart
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
* @param string[]|null $language
|
||||
*/
|
||||
public function __construct(
|
||||
string $subtype,
|
||||
array $attributes,
|
||||
?string $id,
|
||||
?string $description,
|
||||
string $encoding,
|
||||
int $size,
|
||||
public int $textLines,
|
||||
?string $md5,
|
||||
?Disposition $disposition,
|
||||
?array $language,
|
||||
?string $location,
|
||||
) {
|
||||
parent::__construct(
|
||||
'TEXT',
|
||||
$subtype,
|
||||
$attributes,
|
||||
$id,
|
||||
$description,
|
||||
$encoding,
|
||||
$size,
|
||||
$md5,
|
||||
$disposition,
|
||||
$language,
|
||||
$location,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data\Fetch;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class Envelope
|
||||
{
|
||||
/**
|
||||
* @param Address[]|null $from
|
||||
* @param Address[]|null $sender
|
||||
* @param Address[]|null $replyTo
|
||||
* @param Address[]|null $to
|
||||
* @param Address[]|null $cc
|
||||
* @param Address[]|null $bcc
|
||||
*/
|
||||
public function __construct(
|
||||
public ?DateTimeImmutable $date,
|
||||
public ?string $subject,
|
||||
public ?array $from,
|
||||
public ?array $sender,
|
||||
public ?array $replyTo,
|
||||
public ?array $to,
|
||||
public ?array $cc,
|
||||
public ?array $bcc,
|
||||
public ?string $inReplyTo,
|
||||
public ?string $messageId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodySection;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\Envelope;
|
||||
|
||||
final readonly class FetchData implements Data
|
||||
{
|
||||
/**
|
||||
* @param array<string>|null $flags
|
||||
* @param BodySection[] $bodySections
|
||||
*/
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public ?array $flags = null,
|
||||
public ?\DateTimeImmutable $internalDate = null,
|
||||
public ?Envelope $envelope = null,
|
||||
public ?int $rfc822Size = null,
|
||||
public ?string $rfc822 = null,
|
||||
public ?int $uid = null,
|
||||
public ?BodyStructure $bodyStructure = null,
|
||||
public array $bodySections = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function getBodySection(string $name): ?BodySection
|
||||
{
|
||||
foreach (($this->bodySections ?? []) as $bodySection) {
|
||||
if ($bodySection->section == $name) {
|
||||
return $bodySection;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||
|
||||
final readonly class FlagsData implements Data
|
||||
{
|
||||
/**
|
||||
* @param list<string> $flags
|
||||
*/
|
||||
public function __construct(public array $flags)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||
|
||||
final class ListData implements Data
|
||||
{
|
||||
/**
|
||||
* @param list<string> $nameAttributes
|
||||
*/
|
||||
public function __construct(
|
||||
public array $nameAttributes,
|
||||
public string $hierarchyDelimiter,
|
||||
public string $name
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||
|
||||
final class RecentData implements Data
|
||||
{
|
||||
public function __construct(public int $numberOfMessages)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||
|
||||
final readonly class SearchData implements Data
|
||||
{
|
||||
/**
|
||||
* @param list<int> $numbers
|
||||
*/
|
||||
public function __construct(public array $numbers)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line;
|
||||
|
||||
interface Line
|
||||
{
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||
|
||||
final readonly class AppendUidCode implements Code
|
||||
{
|
||||
public function __construct(
|
||||
public int $uidValidity,
|
||||
public int $uid,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||
|
||||
interface Code
|
||||
{
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||
|
||||
final readonly class PermanentFlagsCode implements Code
|
||||
{
|
||||
/**
|
||||
* @param string[] $flags
|
||||
*/
|
||||
public function __construct(
|
||||
public array $flags,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||
|
||||
final readonly class ReadOnlyCode implements Code
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||
|
||||
final readonly class ReadWriteCode implements Code
|
||||
{
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||
|
||||
final readonly class UidNextCode implements Code
|
||||
{
|
||||
public function __construct(
|
||||
public int $value,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||
|
||||
final readonly class UidValidityCode implements Code
|
||||
{
|
||||
public function __construct(
|
||||
public int $value,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||
|
||||
final readonly class UnseenCode implements Code
|
||||
{
|
||||
public function __construct(
|
||||
public int $seq,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status;
|
||||
|
||||
use Gricob\IMAP\Protocol\Response\Line\Line;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Code\Code;
|
||||
|
||||
final readonly class Status implements Line
|
||||
{
|
||||
final public function __construct(
|
||||
public string $tag,
|
||||
public StatusType $type,
|
||||
public ?Code $code,
|
||||
public string $message
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Line\Status;
|
||||
|
||||
enum StatusType: string
|
||||
{
|
||||
case OK = 'OK';
|
||||
case NO = 'NO';
|
||||
case BAD = 'BAD';
|
||||
case PREAUTH = 'PREAUTH';
|
||||
case BYE = 'BYE';
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Parser;
|
||||
|
||||
use Doctrine\Common\Lexer\AbstractLexer;
|
||||
|
||||
/**
|
||||
* @extends AbstractLexer<TokenType, string>
|
||||
*/
|
||||
class Lexer extends AbstractLexer
|
||||
{
|
||||
protected function getCatchablePatterns(): array
|
||||
{
|
||||
return [
|
||||
'[a-zA-Z0-9\.\-]+',
|
||||
'\r\n',
|
||||
];
|
||||
}
|
||||
|
||||
protected function getNonCatchablePatterns(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getType(string &$value)
|
||||
{
|
||||
$normalizedValue = strtoupper($value);
|
||||
|
||||
return match($normalizedValue) {
|
||||
' ' => TokenType::SP,
|
||||
'.' => TokenType::DOT,
|
||||
'*' => TokenType::ASTERISK,
|
||||
'%' => TokenType::PERCENT_SIGN,
|
||||
'+' => TokenType::PLUS_SIGN,
|
||||
'=' => TokenType::EQUALS_SIGN,
|
||||
'"' => TokenType::DOUBLE_QUOTE,
|
||||
'[' => TokenType::OPEN_BRACKETS,
|
||||
']' => TokenType::CLOSE_BRACKETS,
|
||||
'{' => TokenType::OPEN_BRACES,
|
||||
'}' => TokenType::CLOSE_BRACES,
|
||||
'(' => TokenType::OPEN_PARENTHESIS,
|
||||
')' => TokenType::CLOSE_PARENTHESIS,
|
||||
'\\' => TokenType::BACKSLASH,
|
||||
"\r\n" => TokenType::CRLF,
|
||||
'NIL' => TokenType::NIL,
|
||||
'OK', 'NO', 'BAD', 'BYE', 'PREAUTH' => TokenType::STATUS,
|
||||
'APPENDUID' => TokenType::APPENDUID,
|
||||
'UNSEEN' => TokenType::UNSEEN,
|
||||
'UIDVALIDITY' => TokenType::UIDVALIDITY,
|
||||
'UIDNEXT' => TokenType::UIDNEXT,
|
||||
'PERMANENTFLAGS' => TokenType::PERMANENTFLAGS,
|
||||
'READ-WRITE' => TokenType::READ_WRITE,
|
||||
'READ-ONLY' => TokenType::READ_ONLY,
|
||||
'CAPABILITY' => TokenType::CAPABILITY,
|
||||
'LIST' => TokenType::LIST,
|
||||
'FLAGS' => TokenType::FLAGS,
|
||||
'RECENT' => TokenType::RECENT,
|
||||
'FETCH' => TokenType::FETCH,
|
||||
'INTERNALDATE' => TokenType::INTERNALDATE,
|
||||
'SEARCH' => TokenType::SEARCH,
|
||||
'EXISTS' => TokenType::EXISTS,
|
||||
'EXPUNGE' => TokenType::EXPUNGE,
|
||||
'BODY' => TokenType::BODY,
|
||||
'BODYSTRUCTURE' => TokenType::BODYSTRUCTURE,
|
||||
'ENVELOPE' => TokenType::ENVELOPE,
|
||||
'RFC822' => TokenType::RFC822,
|
||||
'RFC822.SIZE' => TokenType::RFC822_SIZE,
|
||||
'RFC822.TEXT' => TokenType::RFC822_TEXT,
|
||||
'RFC822.HEAD' => TokenType::RFC822_HEAD,
|
||||
'UID' => TokenType::UID,
|
||||
default => match (true) {
|
||||
is_numeric($value) => TokenType::NUMBER,
|
||||
ctype_alnum($value) => TokenType::ALPHANUMERIC,
|
||||
ctype_cntrl($value) => TokenType::CTL,
|
||||
default => TokenType::UNKNOWN,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Parser;
|
||||
|
||||
final class ParseError extends \Exception
|
||||
{
|
||||
/**
|
||||
* @param TokenType[] $expected
|
||||
*/
|
||||
public static function unexpectedToken(?TokenType $given, array $expected, string $input): self
|
||||
{
|
||||
return new self(
|
||||
sprintf(
|
||||
"Expected token of type %s. Given %s.\n%s",
|
||||
implode(
|
||||
' or ',
|
||||
array_map(fn (TokenType $type) => $type->name, $expected)
|
||||
),
|
||||
$given?->name ?? 'null',
|
||||
$input
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response\Parser;
|
||||
|
||||
enum TokenType
|
||||
{
|
||||
case SP;
|
||||
case DOT;
|
||||
case ASTERISK;
|
||||
case PERCENT_SIGN;
|
||||
case PLUS_SIGN;
|
||||
case EQUALS_SIGN;
|
||||
case DOUBLE_QUOTE;
|
||||
case NUMBER;
|
||||
case ALPHANUMERIC;
|
||||
case NIL;
|
||||
case OPEN_BRACKETS;
|
||||
case CLOSE_BRACKETS;
|
||||
case OPEN_BRACES;
|
||||
case CLOSE_BRACES;
|
||||
case OPEN_PARENTHESIS;
|
||||
case CLOSE_PARENTHESIS;
|
||||
case BACKSLASH;
|
||||
case CRLF;
|
||||
case CTL;
|
||||
|
||||
case STATUS;
|
||||
|
||||
case APPENDUID;
|
||||
case UNSEEN;
|
||||
case UIDVALIDITY;
|
||||
case UIDNEXT;
|
||||
case PERMANENTFLAGS;
|
||||
case READ_WRITE;
|
||||
case READ_ONLY;
|
||||
|
||||
case CAPABILITY;
|
||||
case LIST;
|
||||
case FLAGS;
|
||||
case INTERNALDATE;
|
||||
case RECENT;
|
||||
case FETCH;
|
||||
case SEARCH;
|
||||
case EXISTS;
|
||||
case EXPUNGE;
|
||||
case BODY;
|
||||
case BODYSTRUCTURE;
|
||||
case ENVELOPE;
|
||||
case RFC822;
|
||||
case RFC822_SIZE;
|
||||
case RFC822_HEAD;
|
||||
case RFC822_TEXT;
|
||||
case UID;
|
||||
|
||||
case UNKNOWN;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response;
|
||||
|
||||
use Gricob\IMAP\Protocol\Response\Line\Line;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
||||
|
||||
final readonly class Response
|
||||
{
|
||||
/**
|
||||
* @param list<Line> $data
|
||||
*/
|
||||
public function __construct(
|
||||
public Status $status,
|
||||
public array $data,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of Line
|
||||
* @param class-string<T> $type
|
||||
* @return T[]
|
||||
*/
|
||||
public function getData(string $type): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->data as $data) {
|
||||
if ($data instanceof $type) {
|
||||
$result[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol\Response;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Line;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
||||
|
||||
class ResponseBuilder
|
||||
{
|
||||
private ?Status $status = null;
|
||||
|
||||
/**
|
||||
* @var list<Line>
|
||||
*/
|
||||
private array $data = [];
|
||||
|
||||
public function __construct(private readonly string $statusTag)
|
||||
{
|
||||
}
|
||||
|
||||
public function addLine(Line $line): void
|
||||
{
|
||||
if ($line instanceof Status && $line->tag === $this->statusTag) {
|
||||
$this->status = $line;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->data[] = $line;
|
||||
}
|
||||
|
||||
public function hasStatus(): bool
|
||||
{
|
||||
return $this->status !== null;
|
||||
}
|
||||
|
||||
public function build(): Response
|
||||
{
|
||||
if (null === $this->status) {
|
||||
throw new BadMethodCallException();
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->status,
|
||||
$this->data,
|
||||
);
|
||||
}
|
||||
}
|
||||
10
lib/Client/Protocol/Response/ResponseInterface.php
Normal file
10
lib/Client/Protocol/Response/ResponseInterface.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol\Response;
|
||||
|
||||
interface ResponseInterface
|
||||
{
|
||||
public function raw(): string;
|
||||
}
|
||||
40
lib/Client/Protocol/Response/TaggedResponse.php
Normal file
40
lib/Client/Protocol/Response/TaggedResponse.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol\Response;
|
||||
|
||||
final class TaggedResponse implements ResponseInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $tag,
|
||||
private readonly string $status,
|
||||
private readonly string $text,
|
||||
private readonly string $raw,
|
||||
) {}
|
||||
|
||||
public function tag(): string
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
public function status(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function text(): string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
public function isOk(): bool
|
||||
{
|
||||
return $this->status === 'OK';
|
||||
}
|
||||
|
||||
public function raw(): string
|
||||
{
|
||||
return $this->raw;
|
||||
}
|
||||
}
|
||||
43
lib/Client/Protocol/Response/UntaggedResponse.php
Normal file
43
lib/Client/Protocol/Response/UntaggedResponse.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol\Response;
|
||||
|
||||
final class UntaggedResponse implements ResponseInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $label,
|
||||
private readonly string $payload,
|
||||
private readonly string $raw,
|
||||
) {}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function payload(): string
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function payloadTokens(): array
|
||||
{
|
||||
$payload = trim($this->payload);
|
||||
|
||||
if ($payload === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return preg_split('/\s+/', $payload) ?: [];
|
||||
}
|
||||
|
||||
public function raw(): string
|
||||
{
|
||||
return $this->raw;
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol;
|
||||
|
||||
use Generator;
|
||||
use Gricob\IMAP\Protocol\Response\Line\CommandContinuation;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Line;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\StatusType;
|
||||
use Gricob\IMAP\Protocol\Response\Parser\Parser;
|
||||
use Gricob\IMAP\Protocol\Response\Response;
|
||||
use Gricob\IMAP\Protocol\Response\ResponseBuilder;
|
||||
use Gricob\IMAP\Transport\ResponseStream;
|
||||
use RuntimeException;
|
||||
|
||||
readonly class ResponseHandler
|
||||
{
|
||||
/**
|
||||
* Literals larger than this threshold (in bytes) are streamed into a
|
||||
* temporary file instead of being held as a PHP string. This prevents
|
||||
* the Doctrine Lexer from running preg_split() over multi-megabyte bodies,
|
||||
* which is the root cause of OOM errors on large mailboxes.
|
||||
*/
|
||||
private const LARGE_LITERAL_THRESHOLD = 524288; // 512 KB
|
||||
|
||||
public function __construct(private Parser $parser)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next complete IMAP response line from $stream.
|
||||
*
|
||||
* Large literals (>= LARGE_LITERAL_THRESHOLD bytes) are read in 8 KB
|
||||
* chunks into php://temp resources instead of being appended to $raw,
|
||||
* so the body content never reaches the lexer as a plain string.
|
||||
*
|
||||
* @return array{string, list<resource>} [$raw, $preloadedLiterals]
|
||||
*/
|
||||
private function readNextRaw(ResponseStream $stream): array
|
||||
{
|
||||
$raw = $stream->readLine();
|
||||
$preloaded = [];
|
||||
|
||||
while (preg_match('/\{(?<bytes>\d+)}\r\n$/', $raw, $matches)) {
|
||||
$literalSize = (int) $matches['bytes'];
|
||||
|
||||
if ($literalSize >= self::LARGE_LITERAL_THRESHOLD) {
|
||||
// Stream into a temp file to avoid holding a huge string in
|
||||
// memory. php://temp uses RAM up to 2 MB then spills to disk.
|
||||
$tmp = fopen('php://temp', 'r+');
|
||||
$remaining = $literalSize;
|
||||
while ($remaining > 0) {
|
||||
$chunk = $stream->read(min(8192, $remaining));
|
||||
fwrite($tmp, $chunk);
|
||||
$remaining -= strlen($chunk);
|
||||
}
|
||||
rewind($tmp);
|
||||
$preloaded[] = $tmp;
|
||||
// Keep the {N}\r\n header in $raw so the parser can read the
|
||||
// literal size, but do NOT append the N bytes — the parser
|
||||
// will pull them from the preloaded resource instead.
|
||||
} else {
|
||||
$raw .= $stream->read($literalSize);
|
||||
}
|
||||
|
||||
$raw .= $stream->readLine();
|
||||
}
|
||||
|
||||
return [$raw, $preloaded];
|
||||
}
|
||||
|
||||
public function handle(string $statusTag, ResponseStream $stream, ContinuationHandler $continuationHandler): Response
|
||||
{
|
||||
$responseBuilder = new ResponseBuilder($statusTag);
|
||||
|
||||
do {
|
||||
[$raw, $preloaded] = $this->readNextRaw($stream);
|
||||
$line = $this->parser->parse($raw, $preloaded);
|
||||
|
||||
if ($line instanceof CommandContinuation) {
|
||||
$continuationHandler->continue();
|
||||
continue;
|
||||
}
|
||||
|
||||
$responseBuilder->addLine($line);
|
||||
} while (!$responseBuilder->hasStatus());
|
||||
|
||||
return $responseBuilder->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams parsed response lines one at a time as a Generator, yielding each
|
||||
* untagged Line immediately as it arrives from the socket. The terminal
|
||||
* Status line is NOT yielded; instead it is set as the generator return
|
||||
* value so callers can retrieve it via $gen->getReturn() after exhaustion.
|
||||
*
|
||||
* @throws CommandFailed if the tagged status is NO or BAD
|
||||
*
|
||||
* @return Generator<int, Line, mixed, Status>
|
||||
*/
|
||||
public function stream(string $statusTag, ResponseStream $stream, ContinuationHandler $continuationHandler): Generator
|
||||
{
|
||||
$status = null;
|
||||
|
||||
do {
|
||||
[$raw, $preloaded] = $this->readNextRaw($stream);
|
||||
$line = $this->parser->parse($raw, $preloaded);
|
||||
|
||||
if ($line instanceof CommandContinuation) {
|
||||
$continuationHandler->continue();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($line instanceof Status && $line->tag === $statusTag) {
|
||||
$status = $line;
|
||||
break;
|
||||
}
|
||||
|
||||
yield $line;
|
||||
} while (true);
|
||||
|
||||
if ($status->type !== StatusType::OK) {
|
||||
throw CommandFailed::withStatus($status);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
28
lib/Client/Protocol/ResponseStream.php
Normal file
28
lib/Client/Protocol/ResponseStream.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Protocol;
|
||||
|
||||
use Generator;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
final class ResponseStream implements IteratorAggregate
|
||||
{
|
||||
/** @var \Closure():Generator */
|
||||
private readonly \Closure $generatorFactory;
|
||||
|
||||
/**
|
||||
* @param \Closure():Generator $generatorFactory
|
||||
*/
|
||||
public function __construct(\Closure $generatorFactory)
|
||||
{
|
||||
$this->generatorFactory = $generatorFactory;
|
||||
}
|
||||
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return ($this->generatorFactory)();
|
||||
}
|
||||
}
|
||||
@@ -2,35 +2,14 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol;
|
||||
namespace KTXM\ProviderImap\Client\Protocol;
|
||||
|
||||
final class TagGenerator
|
||||
{
|
||||
private const MAX_NUMBER = 999;
|
||||
private const NUMBER_PART_LENGTH = 3;
|
||||
private const INITIAL_LETTER = 'A';
|
||||
private const INITIAL_NUMBER = 0;
|
||||
|
||||
private string $letter = self::INITIAL_LETTER;
|
||||
private int $number = self::INITIAL_NUMBER;
|
||||
private int $counter = 1;
|
||||
|
||||
public function next(): string
|
||||
{
|
||||
$this->number += 1;
|
||||
|
||||
if ($this->number > self::MAX_NUMBER) {
|
||||
$this->letter++;
|
||||
$this->number = self::INITIAL_NUMBER;
|
||||
}
|
||||
|
||||
if (strlen($this->letter) > 1) {
|
||||
$this->letter = self::INITIAL_LETTER;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%s%s',
|
||||
$this->letter,
|
||||
str_pad((string) $this->number, self::NUMBER_PART_LENGTH, '0', STR_PAD_LEFT)
|
||||
);
|
||||
return sprintf('A%04d', $this->counter++);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Protocol;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class UnexpectedContinuationHandler implements ContinuationHandler
|
||||
{
|
||||
public function continue(): void
|
||||
{
|
||||
throw new RuntimeException('Unexpected continuation response');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user