generated from Nodarx/template
feat: initial version
Signed-off-by: Sebastian Krupinski <root@LAPTOP-7DVOR6NC>
This commit was merged in pull request #1.
This commit is contained in:
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Frontend development
|
||||||
|
node_modules/
|
||||||
|
*.local
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.cache/
|
||||||
|
.vite/
|
||||||
|
.temp/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Frontend build
|
||||||
|
/static/
|
||||||
|
|
||||||
|
# Backend development
|
||||||
|
/lib/vendor/
|
||||||
|
coverage/
|
||||||
|
phpunit.xml.cache
|
||||||
|
.phpunit.cache
|
||||||
|
.phpunit.result.cache
|
||||||
|
.php-cs-fixer.cache
|
||||||
|
.phpstan.cache
|
||||||
|
.phpactor/
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Old files
|
||||||
|
lib.old/
|
||||||
|
src.old/
|
||||||
42
README.md
Normal file
42
README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# IMAP Mail Provider Module
|
||||||
|
|
||||||
|
This module provides an implementation of an IMAP mail provider using the `gricob/imap` library. It is designed to facilitate email operations such as managing mailboxes and messages through the IMAP protocol.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Service Location**: Configures connection details including host, port, and encryption type.
|
||||||
|
- **Service Identity**: Manages user credentials securely.
|
||||||
|
- **Mailbox Management**: Supports operations for listing, fetching, creating, modifying, and deleting mailboxes and messages.
|
||||||
|
- **Autodiscovery**: Implements methods for discovering IMAP services automatically.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install the module, run the following command in the module directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install the required dependencies, including `gricob/imap`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Service Test**: Use the `serviceTest()` method to check connectivity to the IMAP server.
|
||||||
|
2. **Discover Services**: Call `serviceDiscover()` to find available mail services.
|
||||||
|
3. **Mailbox Operations**: Utilize the `RemoteMailService` to perform operations such as listing mailboxes and managing messages.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
$provider = new KTXM\ProviderImapMail\Providers\Provider();
|
||||||
|
$provider->serviceTest();
|
||||||
|
$mailboxes = $provider->serviceDiscover();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please submit a pull request or open an issue for any enhancements or bug fixes.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the LICENSE file for details.
|
||||||
47
composer.json
Normal file
47
composer.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "ktxm/provider-imap-mail",
|
||||||
|
"description": "IMAP Mail Provider Module",
|
||||||
|
"type": "library",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"platform": {
|
||||||
|
"php": "8.2"
|
||||||
|
},
|
||||||
|
"autoloader-suffix": "ProviderImapMail",
|
||||||
|
"vendor-dir": "lib/vendor",
|
||||||
|
"allow-plugins": {
|
||||||
|
"bamarni/composer-bin-plugin": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2 <=8.5",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"psr/log": "^1.0|^2.0|^3.0",
|
||||||
|
"doctrine/lexer": "^3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^11.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"KTXM\\ProviderImapMail\\": "lib/",
|
||||||
|
"Gricob\\IMAP\\": "lib/Client"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"KTXT\\ProviderImapMail\\": "tests/php/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"post-install-cmd": [
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
],
|
||||||
|
"test:unit": "phpunit --configuration tests/php/phpunit.unit.xml --colors=always --testdox",
|
||||||
|
"test:coverage": "XDEBUG_MODE=coverage phpunit --configuration tests/php/phpunit.unit.xml --coverage-html .phpunit.coverage --coverage-text"
|
||||||
|
}
|
||||||
|
}
|
||||||
1935
composer.lock
generated
Normal file
1935
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
515
lib/Client/Client.php
Normal file
515
lib/Client/Client.php
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Exception;
|
||||||
|
use Generator;
|
||||||
|
use Gricob\IMAP\Mime\LazyMessage;
|
||||||
|
use Gricob\IMAP\Mime\Message;
|
||||||
|
use Gricob\IMAP\Mime\Part\Disposition;
|
||||||
|
use Gricob\IMAP\Mime\Part\LazyBody;
|
||||||
|
use Gricob\IMAP\Mime\Part\MultiPart;
|
||||||
|
use Gricob\IMAP\Mime\Part\Part;
|
||||||
|
use Gricob\IMAP\Mime\Part\SinglePart;
|
||||||
|
use Gricob\IMAP\Protocol\Command\AppendCommand;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\Search\Criteria;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\SequenceSet;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\Store\Flags;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Authenticate\SASLMechanism;
|
||||||
|
use Gricob\IMAP\Protocol\Command\AuthenticateCommand;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Command;
|
||||||
|
use Gricob\IMAP\Protocol\Command\CreateCommand;
|
||||||
|
use Gricob\IMAP\Protocol\Command\ExpungeCommand;
|
||||||
|
use Gricob\IMAP\Protocol\Command\FetchCommand;
|
||||||
|
use Gricob\IMAP\Protocol\Command\ListCommand;
|
||||||
|
use Gricob\IMAP\Protocol\Command\LogInCommand;
|
||||||
|
use Gricob\IMAP\Protocol\Command\SelectCommand;
|
||||||
|
use Gricob\IMAP\Protocol\Command\StoreCommand;
|
||||||
|
use Gricob\IMAP\Protocol\Imap;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Data\FetchData;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Data\FlagsData;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Data\ExistsData;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Data\RecentData;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Data\ListData;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Data\SearchData;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Status\Code\AppendUidCode;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Status\Code\PermanentFlagsCode;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Status\Code\UidNextCode;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Status\Code\UidValidityCode;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Status\Code\UnseenCode;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
||||||
|
use Gricob\IMAP\Protocol\Response\Response;
|
||||||
|
use Gricob\IMAP\Transport\Socket\SocketConnection;
|
||||||
|
use Gricob\IMAP\Transport\Traceable\TraceableConnection;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class Client
|
||||||
|
{
|
||||||
|
public Configuration $configuration;
|
||||||
|
private Imap $imap;
|
||||||
|
|
||||||
|
private Mailbox $selectedMailbox;
|
||||||
|
|
||||||
|
private function __construct(
|
||||||
|
Configuration $configuration,
|
||||||
|
?LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
$connection = new SocketConnection(
|
||||||
|
$configuration->transport,
|
||||||
|
$configuration->host,
|
||||||
|
$configuration->port,
|
||||||
|
$configuration->timeout,
|
||||||
|
$configuration->verifyPeer,
|
||||||
|
$configuration->verifyPeerName,
|
||||||
|
$configuration->allowSelfSigned,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (null !== $logger) {
|
||||||
|
$connection = new TraceableConnection($connection, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->configuration = $configuration;
|
||||||
|
$this->imap = new Imap($connection);
|
||||||
|
$this->selectedMailbox = new Mailbox([], '', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create(Configuration $configuration, ?LoggerInterface $logger = null): self
|
||||||
|
{
|
||||||
|
return new self($configuration, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function connect(): void
|
||||||
|
{
|
||||||
|
$this->imap->connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform STARTTLS negotiation (patch).
|
||||||
|
*
|
||||||
|
* Call after connect() but before logIn(). The underlying Imap protocol
|
||||||
|
* layer sends the STARTTLS command and upgrades the socket to TLS.
|
||||||
|
*/
|
||||||
|
public function startTls(): void
|
||||||
|
{
|
||||||
|
$this->imap->startTls();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disconnect(): void
|
||||||
|
{
|
||||||
|
$this->imap->disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logIn(string $username, string $password): void
|
||||||
|
{
|
||||||
|
$this->send(new LogInCommand($username, $password));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(SASLMechanism $mechanism): void
|
||||||
|
{
|
||||||
|
$this->send(new AuthenticateCommand($mechanism));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<Mailbox>
|
||||||
|
*/
|
||||||
|
public function mailboxes(string $referenceName = '', string $pattern = '*'): array
|
||||||
|
{
|
||||||
|
$response = $this->send(new ListCommand($referenceName, $pattern));
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
fn (ListData $data) => new Mailbox($data->nameAttributes, $data->hierarchyDelimiter, $data->name),
|
||||||
|
$response->getData(ListData::class),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function select(Mailbox|string $mailbox): Mailbox
|
||||||
|
{
|
||||||
|
if (is_string($mailbox)) {
|
||||||
|
$mailbox = new Mailbox([], '', $mailbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->send(new SelectCommand($mailbox->name));
|
||||||
|
|
||||||
|
if ($flagsData = $response->getData(FlagsData::class)[0] ?? null) {
|
||||||
|
$mailbox->flags = $flagsData->flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existsData = $response->getData(ExistsData::class)[0] ?? null) {
|
||||||
|
$mailbox->exists = $existsData->numberOfMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($recentData = $response->getData(RecentData::class)[0] ?? null) {
|
||||||
|
$mailbox->recent = $recentData->numberOfMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($response->getData(Status::class) as $status) {
|
||||||
|
if ($status->code instanceof UnseenCode) {
|
||||||
|
$mailbox->unseen = $status->code->seq;
|
||||||
|
} elseif ($status->code instanceof UidValidityCode) {
|
||||||
|
$mailbox->uidValidity = $status->code->value;
|
||||||
|
} elseif ($status->code instanceof UidNextCode) {
|
||||||
|
$mailbox->uidNext = $status->code->value;
|
||||||
|
} elseif ($status->code instanceof PermanentFlagsCode) {
|
||||||
|
$mailbox->permanentFlags = $status->code->flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->selectedMailbox = $mailbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function search(): Search
|
||||||
|
{
|
||||||
|
return new Search($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws MessageNotFound
|
||||||
|
*/
|
||||||
|
public function fetch(int $id): Message
|
||||||
|
{
|
||||||
|
$response = $this->imap->send(
|
||||||
|
new FetchCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
new SequenceSet($id),
|
||||||
|
['INTERNALDATE', 'BODY[HEADER]', 'BODYSTRUCTURE']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = $response->getData(FetchData::class)[0] ?? throw new MessageNotFound();
|
||||||
|
|
||||||
|
if (null === $internalDate = $data->internalDate) {
|
||||||
|
throw new Exception('Unable to fetch internal date from message '.$id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $part = $data->bodyStructure?->part) {
|
||||||
|
throw new Exception('Unable to fetch body structure from message '.$id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Message(
|
||||||
|
$id,
|
||||||
|
$this->createHeaders($data) ?? [],
|
||||||
|
$this->createMessagePart($id, '0', $part),
|
||||||
|
$internalDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream FetchData for a specific set of UIDs, one response line at a time.
|
||||||
|
*
|
||||||
|
* Uses the same sendStreaming path as fetchMultiple() so responses are
|
||||||
|
* processed as they arrive off the socket without buffering the entire
|
||||||
|
* server reply. Items can be tailored per call-site; defaults to a rich
|
||||||
|
* set that populates EntityResource fully (flags, envelope, body structure,
|
||||||
|
* size, arrival date).
|
||||||
|
*
|
||||||
|
* @param int[] $uids
|
||||||
|
* @param string[] $items IMAP fetch data items
|
||||||
|
* @return Generator<int, FetchData> Yields uid => FetchData
|
||||||
|
*/
|
||||||
|
public function streamByUids(
|
||||||
|
array $uids,
|
||||||
|
array $items = ['FLAGS', 'ENVELOPE', 'INTERNALDATE', 'RFC822.SIZE', 'BODYSTRUCTURE', 'UID'],
|
||||||
|
): Generator {
|
||||||
|
$gen = $this->imap->sendStreaming(
|
||||||
|
new FetchCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
new SequenceSet(...$uids),
|
||||||
|
$items,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($gen as $line) {
|
||||||
|
if (!$line instanceof FetchData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $line->id;
|
||||||
|
if ($this->configuration->useUid) {
|
||||||
|
$id = $line->uid ?? throw new RuntimeException('Unable to get uid from message ' . $line->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield $id => $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream messages from a sequence range as a Generator, yielding each
|
||||||
|
* LazyMessage as soon as its FETCH response line arrives off the socket —
|
||||||
|
* without waiting for the entire batch to complete.
|
||||||
|
*
|
||||||
|
* Usage with an NDJSON HTTP response:
|
||||||
|
*
|
||||||
|
* foreach ($client->fetchMultiple(1, 50) as $message) {
|
||||||
|
* echo json_encode($message) . "\n";
|
||||||
|
* flush();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param int $from First sequence number (inclusive)
|
||||||
|
* @param int $to Last sequence number (inclusive)
|
||||||
|
* @return Generator<int, LazyMessage>
|
||||||
|
*/
|
||||||
|
public function fetchMultiple(int $from, int $to): Generator
|
||||||
|
{
|
||||||
|
$items = ['FLAGS', 'INTERNALDATE', 'BODY[HEADER]'];
|
||||||
|
|
||||||
|
$gen = $this->imap->sendStreaming(
|
||||||
|
new FetchCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
SequenceSet::range($from, $to),
|
||||||
|
$items,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($gen as $line) {
|
||||||
|
if (!$line instanceof FetchData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $line->id;
|
||||||
|
if ($this->configuration->useUid) {
|
||||||
|
$id = $line->uid ?? throw new RuntimeException('Unable to get uid from message ' . $line->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield new LazyMessage(
|
||||||
|
$this,
|
||||||
|
$id,
|
||||||
|
$this->createHeaders($line),
|
||||||
|
$line->internalDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
* @throws MessageNotFound
|
||||||
|
*/
|
||||||
|
public function fetchHeaders(int $id): array
|
||||||
|
{
|
||||||
|
$response = $this->imap->send(
|
||||||
|
new FetchCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
new SequenceSet($id),
|
||||||
|
['BODY[HEADER]']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @var FetchData $data */
|
||||||
|
$data = $response->getData(FetchData::class)[0] ?? throw new MessageNotFound();
|
||||||
|
|
||||||
|
return $this->createHeaders($data) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchBody(int $id): Part
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
new FetchCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
new SequenceSet($id),
|
||||||
|
['BODYSTRUCTURE']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = $response->getData(FetchData::class)[0];
|
||||||
|
|
||||||
|
if (null === $part = $data->bodyStructure?->part) {
|
||||||
|
throw new Exception('Unable to fetch body from message '.$id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->createMessagePart($id, '0', $part);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchInternalDate(int $id): DateTimeImmutable
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
new FetchCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
new SequenceSet($id),
|
||||||
|
['INTERNALDATE']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = $response->getData(FetchData::class)[0];
|
||||||
|
|
||||||
|
if (null === $internalDate = $data->internalDate) {
|
||||||
|
throw new Exception('Unable to fetch internal date from message '.$id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $internalDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchSectionBody(int $id, string $section): string
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
new FetchCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
new SequenceSet($id),
|
||||||
|
["BODY[$section]"]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = $response->getData(FetchData::class)[0];
|
||||||
|
|
||||||
|
return $data->getBodySection($section)?->text ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteMessage(Message|int $message): void
|
||||||
|
{
|
||||||
|
$id = $message instanceof Message ? $message->id() : $message;
|
||||||
|
|
||||||
|
$this->send(
|
||||||
|
new StoreCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
new SequenceSet($id),
|
||||||
|
new Flags(['\Deleted'], '+')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->send(new ExpungeCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createMailbox(string $name): void
|
||||||
|
{
|
||||||
|
$this->send(new CreateCommand($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string>|null $flags
|
||||||
|
*/
|
||||||
|
public function append(
|
||||||
|
string $message,
|
||||||
|
string $mailbox = 'INBOX',
|
||||||
|
?array $flags = null,
|
||||||
|
?DateTimeInterface $internalDate = null
|
||||||
|
): int
|
||||||
|
{
|
||||||
|
$response = $this->send(new AppendCommand($mailbox, $message, $flags, $internalDate));
|
||||||
|
|
||||||
|
$code = $response->status->code;
|
||||||
|
if ($code instanceof AppendUidCode) {
|
||||||
|
return $code->uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException('Unable to retrieve uid from append response');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(Command $command): Response
|
||||||
|
{
|
||||||
|
$this->imap->connect();
|
||||||
|
|
||||||
|
return $this->imap->send($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<Criteria> $criteria
|
||||||
|
* @return array<Message>
|
||||||
|
*/
|
||||||
|
public function doSearch(array $criteria, ?PreFetchOptions $preFetchOptions = null): array
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
new Protocol\Command\SearchCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
...$criteria
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$ids = [];
|
||||||
|
foreach ($response->data as $data) {
|
||||||
|
if ($data instanceof SearchData) {
|
||||||
|
array_push($ids, ...$data->numbers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($ids)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $preFetchOptions) {
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
if ($preFetchOptions->headers) {
|
||||||
|
$items[] = 'BODY[HEADER]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($preFetchOptions->internalDate) {
|
||||||
|
$items[] = 'INTERNALDATE';
|
||||||
|
}
|
||||||
|
|
||||||
|
$preFetchResult = $this->send(new FetchCommand(
|
||||||
|
$this->configuration->useUid,
|
||||||
|
new SequenceSet(...$ids),
|
||||||
|
$items,
|
||||||
|
));
|
||||||
|
|
||||||
|
$messages = [];
|
||||||
|
foreach ($preFetchResult->data as $data) {
|
||||||
|
if ($data instanceof FetchData) {
|
||||||
|
$id = $data->id;
|
||||||
|
if ($this->configuration->useUid) {
|
||||||
|
$id = $data->uid ?? throw new RuntimeException('Unable to get uid from message '.$id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$messages[] = new LazyMessage(
|
||||||
|
$this,
|
||||||
|
$id,
|
||||||
|
$this->createHeaders($data),
|
||||||
|
$data->internalDate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map(fn (int $id) => new LazyMessage($this, $id), $ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>|null
|
||||||
|
*/
|
||||||
|
private function createHeaders(FetchData $data): ?array
|
||||||
|
{
|
||||||
|
if (null === $headerSection = $data->getBodySection('HEADER')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconv_mime_decode_headers($headerSection->text, ICONV_MIME_DECODE_CONTINUE_ON_ERROR) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createMessagePart(int $id, string $section, BodyStructure\Part $part): Mime\Part\Part
|
||||||
|
{
|
||||||
|
if ($part instanceof BodyStructure\SinglePart) {
|
||||||
|
return new SinglePart(
|
||||||
|
$part->type,
|
||||||
|
$part->subtype,
|
||||||
|
$part->attributes,
|
||||||
|
new LazyBody($this, $id, $section === '0' ? '1' : $section),
|
||||||
|
$part->attributes['charset'] ?? 'utf-8',
|
||||||
|
$part->encoding,
|
||||||
|
null !== $part->disposition
|
||||||
|
? new Disposition(
|
||||||
|
$part->disposition->type,
|
||||||
|
$part->disposition->attributes['filename'] ?? null
|
||||||
|
) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$part instanceof BodyStructure\MultiPart) {
|
||||||
|
throw new Exception('Unable to create message part from body structure part of class '.$part::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$childParts = [];
|
||||||
|
foreach ($part->parts as $index => $childPart) {
|
||||||
|
$childIndex = (string) ($index + 1);
|
||||||
|
$childSection = $section === '0' ? $childIndex : $section.'.'.$childIndex;
|
||||||
|
$childParts[] = $this->createMessagePart($id, $childSection, $childPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MultiPart($part->subtype, $part->attributes, $childParts);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/Client/Configuration.php
Normal file
20
lib/Client/Configuration.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP;
|
||||||
|
|
||||||
|
final readonly class Configuration
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $transport,
|
||||||
|
public string $host,
|
||||||
|
public int $port = 993,
|
||||||
|
public int $timeout = 60,
|
||||||
|
public bool $verifyPeer = true,
|
||||||
|
public bool $verifyPeerName = true,
|
||||||
|
public bool $allowSelfSigned = false,
|
||||||
|
public bool $useUid = true,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
33
lib/Client/Mailbox.php
Normal file
33
lib/Client/Mailbox.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP;
|
||||||
|
|
||||||
|
class Mailbox
|
||||||
|
{
|
||||||
|
private const ATTRIBUTE_NOSELECT = '\Noselect';
|
||||||
|
|
||||||
|
public array $flags = [];
|
||||||
|
public int $exists = 0;
|
||||||
|
public int $recent = 0;
|
||||||
|
public ?int $unseen = null;
|
||||||
|
public ?int $uidValidity = null;
|
||||||
|
public ?int $uidNext = null;
|
||||||
|
public array $permanentFlags = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $nameAttributes
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public array $nameAttributes,
|
||||||
|
public string $hierarchyDelimiter,
|
||||||
|
public string $name,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSelectable(): bool
|
||||||
|
{
|
||||||
|
return !in_array(self::ATTRIBUTE_NOSELECT, $this->nameAttributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/Client/MessageNotFound.php
Normal file
11
lib/Client/MessageNotFound.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class MessageNotFound extends Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
56
lib/Client/Mime/LazyMessage.php
Normal file
56
lib/Client/Mime/LazyMessage.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Mime;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Gricob\IMAP\Client;
|
||||||
|
use Gricob\IMAP\Mime\Part\Part;
|
||||||
|
|
||||||
|
class LazyMessage extends Message
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Client $client,
|
||||||
|
int $id,
|
||||||
|
?array $headers = null,
|
||||||
|
?DateTimeImmutable $internalDate = null,
|
||||||
|
) {
|
||||||
|
$this->id = $id;
|
||||||
|
|
||||||
|
if (null !== $headers) {
|
||||||
|
$this->headers = $headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $internalDate) {
|
||||||
|
$this->internalDate = $internalDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headers(): array
|
||||||
|
{
|
||||||
|
if (!isset($this->headers)) {
|
||||||
|
$this->headers = $this->client->fetchHeaders($this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::headers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function body(): Part
|
||||||
|
{
|
||||||
|
if (!isset($this->body)) {
|
||||||
|
$this->body = $this->client->fetchBody($this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::body();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function internalDate(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
if (!isset($this->internalDate)) {
|
||||||
|
$this->internalDate = $this->client->fetchInternalDate($this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::internalDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/Client/Mime/Message.php
Normal file
55
lib/Client/Mime/Message.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Mime;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Gricob\IMAP\Mime\Part\Part;
|
||||||
|
|
||||||
|
class Message
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $headers
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected int $id,
|
||||||
|
protected array $headers,
|
||||||
|
protected Part $body,
|
||||||
|
protected DateTimeImmutable $internalDate,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function id(): int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function headers(): array
|
||||||
|
{
|
||||||
|
return $this->headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function body(): Part
|
||||||
|
{
|
||||||
|
return $this->body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function internalDate(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->internalDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function textBody(): ?string
|
||||||
|
{
|
||||||
|
return $this->body()->findPartByMimeType('text/plain')?->decodedBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function htmlBody(): ?string
|
||||||
|
{
|
||||||
|
return $this->body()->findPartByMimeType('text/html')?->decodedBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/Client/Mime/Part/Body.php
Normal file
20
lib/Client/Mime/Part/Body.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Mime\Part;
|
||||||
|
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
class Body implements Stringable
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected string $value
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/Client/Mime/Part/Disposition.php
Normal file
14
lib/Client/Mime/Part/Disposition.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Mime\Part;
|
||||||
|
|
||||||
|
final readonly class Disposition
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $type,
|
||||||
|
public ?string $filename,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/Client/Mime/Part/LazyBody.php
Normal file
26
lib/Client/Mime/Part/LazyBody.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Mime\Part;
|
||||||
|
|
||||||
|
use Gricob\IMAP\Client;
|
||||||
|
|
||||||
|
class LazyBody extends Body
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Client $client,
|
||||||
|
private int $id,
|
||||||
|
private string $section,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
if (!isset($this->value)) {
|
||||||
|
$this->value = $this->client->fetchSectionBody($this->id, $this->section);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/Client/Mime/Part/MultiPart.php
Normal file
31
lib/Client/Mime/Part/MultiPart.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Mime\Part;
|
||||||
|
|
||||||
|
final readonly class MultiPart extends Part
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string,string> $attributes
|
||||||
|
* @param list<Part> $parts
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $subtype,
|
||||||
|
array $attributes,
|
||||||
|
public array $parts,
|
||||||
|
) {
|
||||||
|
parent::__construct('multipart', $subtype, $attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findPartByMimeType(string $mimeType): ?SinglePart
|
||||||
|
{
|
||||||
|
foreach ($this->parts as $part) {
|
||||||
|
if ($matchedPart = $part->findPartByMimeType(strtolower($mimeType))) {
|
||||||
|
return $matchedPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/Client/Mime/Part/Part.php
Normal file
36
lib/Client/Mime/Part/Part.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Mime\Part;
|
||||||
|
|
||||||
|
abstract readonly class Part
|
||||||
|
{
|
||||||
|
public string $type;
|
||||||
|
public string $subtype;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
public array $attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,string> $attributes
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $type,
|
||||||
|
string $subtype,
|
||||||
|
array $attributes,
|
||||||
|
) {
|
||||||
|
$this->subtype = strtolower($subtype);
|
||||||
|
$this->type = strtolower($type);
|
||||||
|
$this->attributes = $attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract public function findPartByMimeType(string $mimeType): ?SinglePart;
|
||||||
|
|
||||||
|
public function mimeType(): string
|
||||||
|
{
|
||||||
|
return $this->type.'/'.$this->subtype;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
lib/Client/Mime/Part/SinglePart.php
Normal file
62
lib/Client/Mime/Part/SinglePart.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Mime\Part;
|
||||||
|
|
||||||
|
final readonly class SinglePart extends Part
|
||||||
|
{
|
||||||
|
private string $encoding;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $type,
|
||||||
|
string $subtype,
|
||||||
|
array $attributes,
|
||||||
|
private Body $body,
|
||||||
|
private string $charset,
|
||||||
|
string $encoding,
|
||||||
|
private ?Disposition $disposition,
|
||||||
|
) {
|
||||||
|
|
||||||
|
$this->encoding = strtolower($encoding);
|
||||||
|
parent::__construct($type, $subtype, $attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function body(): string
|
||||||
|
{
|
||||||
|
return (string) $this->body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decodedBody(): string
|
||||||
|
{
|
||||||
|
return match ($this->encoding) {
|
||||||
|
'quoted-printable' => quoted_printable_decode($this->body()),
|
||||||
|
'base64' => base64_decode($this->body()),
|
||||||
|
default => $this->body(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function charset(): string
|
||||||
|
{
|
||||||
|
return $this->charset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function encoding(): string
|
||||||
|
{
|
||||||
|
return $this->encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disposition(): ?Disposition
|
||||||
|
{
|
||||||
|
return $this->disposition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findPartByMimeType(string $mimeType): ?SinglePart
|
||||||
|
{
|
||||||
|
if ($this->mimeType() === strtolower($mimeType)) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/Client/PreFetchOptions.php
Normal file
14
lib/Client/PreFetchOptions.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP;
|
||||||
|
|
||||||
|
final readonly class PreFetchOptions
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public bool $internalDate = false,
|
||||||
|
public bool $headers = false,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/Client/Protocol/Command/AppendCommand.php
Normal file
39
lib/Client/Protocol/Command/AppendCommand.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
lib/Client/Protocol/Command/Argument/Argument.php
Normal file
10
lib/Client/Protocol/Command/Argument/Argument.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Command\Argument;
|
||||||
|
|
||||||
|
interface Argument
|
||||||
|
{
|
||||||
|
public function __toString(): string;
|
||||||
|
}
|
||||||
24
lib/Client/Protocol/Command/Argument/Date.php
Normal file
24
lib/Client/Protocol/Command/Argument/Date.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/Client/Protocol/Command/Argument/DateTime.php
Normal file
24
lib/Client/Protocol/Command/Argument/DateTime.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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').'"';
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/Client/Protocol/Command/Argument/ParenthesizedList.php
Normal file
28
lib/Client/Protocol/Command/Argument/ParenthesizedList.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lib/Client/Protocol/Command/Argument/QuotedString.php
Normal file
17
lib/Client/Protocol/Command/Argument/QuotedString.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
lib/Client/Protocol/Command/Argument/Search/All.php
Normal file
13
lib/Client/Protocol/Command/Argument/Search/All.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||||
|
|
||||||
|
class All implements Criteria
|
||||||
|
{
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return 'ALL';
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/Client/Protocol/Command/Argument/Search/Before.php
Normal file
15
lib/Client/Protocol/Command/Argument/Search/Before.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/Client/Protocol/Command/Argument/Search/Criteria.php
Normal file
11
lib/Client/Protocol/Command/Argument/Search/Criteria.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Command\Argument\Search;
|
||||||
|
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\Argument;
|
||||||
|
|
||||||
|
interface Criteria extends Argument
|
||||||
|
{
|
||||||
|
}
|
||||||
19
lib/Client/Protocol/Command/Argument/Search/Header.php
Normal file
19
lib/Client/Protocol/Command/Argument/Search/Header.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lib/Client/Protocol/Command/Argument/Search/Not.php
Normal file
17
lib/Client/Protocol/Command/Argument/Search/Not.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?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.')';
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/Client/Protocol/Command/Argument/Search/Since.php
Normal file
15
lib/Client/Protocol/Command/Argument/Search/Since.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/Client/Protocol/Command/Argument/SequenceSet.php
Normal file
36
lib/Client/Protocol/Command/Argument/SequenceSet.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
if ($this->range !== null) {
|
||||||
|
return $this->range;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(',', $this->numbers);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
lib/Client/Protocol/Command/Argument/Store/Flags.php
Normal file
30
lib/Client/Protocol/Command/Argument/Store/Flags.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/Client/Protocol/Command/Authenticate/SASLMechanism.php
Normal file
12
lib/Client/Protocol/Command/Authenticate/SASLMechanism.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
}
|
||||||
26
lib/Client/Protocol/Command/Authenticate/XOAuth2.php
Normal file
26
lib/Client/Protocol/Command/Authenticate/XOAuth2.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/Client/Protocol/Command/AuthenticateCommand.php
Normal file
20
lib/Client/Protocol/Command/AuthenticateCommand.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
40
lib/Client/Protocol/Command/Command.php
Normal file
40
lib/Client/Protocol/Command/Command.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
lib/Client/Protocol/Command/Continuable.php
Normal file
10
lib/Client/Protocol/Command/Continuable.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Command;
|
||||||
|
|
||||||
|
interface Continuable
|
||||||
|
{
|
||||||
|
public function continue(): string;
|
||||||
|
}
|
||||||
15
lib/Client/Protocol/Command/CreateCommand.php
Normal file
15
lib/Client/Protocol/Command/CreateCommand.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
13
lib/Client/Protocol/Command/ExpungeCommand.php
Normal file
13
lib/Client/Protocol/Command/ExpungeCommand.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Command;
|
||||||
|
|
||||||
|
final readonly class ExpungeCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('EXPUNGE');
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/Client/Protocol/Command/FetchCommand.php
Normal file
28
lib/Client/Protocol/Command/FetchCommand.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/Client/Protocol/Command/ListCommand.php
Normal file
19
lib/Client/Protocol/Command/ListCommand.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/Client/Protocol/Command/LogInCommand.php
Normal file
19
lib/Client/Protocol/Command/LogInCommand.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/Client/Protocol/Command/SearchCommand.php
Normal file
20
lib/Client/Protocol/Command/SearchCommand.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/Client/Protocol/Command/SelectCommand.php
Normal file
15
lib/Client/Protocol/Command/SelectCommand.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/Client/Protocol/Command/StartTlsCommand.php
Normal file
19
lib/Client/Protocol/Command/StartTlsCommand.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
23
lib/Client/Protocol/Command/StoreCommand.php
Normal file
23
lib/Client/Protocol/Command/StoreCommand.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
lib/Client/Protocol/CommandFailed.php
Normal file
16
lib/Client/Protocol/CommandFailed.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
70
lib/Client/Protocol/CommandInteraction.php
Normal file
70
lib/Client/Protocol/CommandInteraction.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?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");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/Client/Protocol/ConnectionRejected.php
Normal file
11
lib/Client/Protocol/ConnectionRejected.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class ConnectionRejected extends RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
10
lib/Client/Protocol/ContinuationHandler.php
Normal file
10
lib/Client/Protocol/ContinuationHandler.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol;
|
||||||
|
|
||||||
|
interface ContinuationHandler
|
||||||
|
{
|
||||||
|
public function continue(): void;
|
||||||
|
}
|
||||||
129
lib/Client/Protocol/Imap.php
Normal file
129
lib/Client/Protocol/Imap.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
lib/Client/Protocol/Response/Line/CommandContinuation.php
Normal file
13
lib/Client/Protocol/Response/Line/CommandContinuation.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Response\Line;
|
||||||
|
|
||||||
|
final readonly class CommandContinuation implements Line
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $message,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/Client/Protocol/Response/Line/Data/CapabilityData.php
Normal file
15
lib/Client/Protocol/Response/Line/Data/CapabilityData.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/Client/Protocol/Response/Line/Data/Data.php
Normal file
11
lib/Client/Protocol/Response/Line/Data/Data.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||||
|
|
||||||
|
use Gricob\IMAP\Protocol\Response\Line\Line;
|
||||||
|
|
||||||
|
interface Data extends Line
|
||||||
|
{
|
||||||
|
}
|
||||||
12
lib/Client/Protocol/Response/Line/Data/ExistsData.php
Normal file
12
lib/Client/Protocol/Response/Line/Data/ExistsData.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||||
|
|
||||||
|
final readonly class ExistsData implements Data
|
||||||
|
{
|
||||||
|
public function __construct(public int $numberOfMessages)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/Client/Protocol/Response/Line/Data/ExpungeData.php
Normal file
12
lib/Client/Protocol/Response/Line/Data/ExpungeData.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||||
|
|
||||||
|
final readonly class ExpungeData implements Data
|
||||||
|
{
|
||||||
|
public function __construct(public int $id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
16
lib/Client/Protocol/Response/Line/Data/Fetch/Address.php
Normal file
16
lib/Client/Protocol/Response/Line/Data/Fetch/Address.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/Client/Protocol/Response/Line/Data/Fetch/BodySection.php
Normal file
12
lib/Client/Protocol/Response/Line/Data/Fetch/BodySection.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
lib/Client/Protocol/Response/Line/Data/Fetch/Envelope.php
Normal file
32
lib/Client/Protocol/Response/Line/Data/Fetch/Envelope.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
40
lib/Client/Protocol/Response/Line/Data/FetchData.php
Normal file
40
lib/Client/Protocol/Response/Line/Data/FetchData.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/Client/Protocol/Response/Line/Data/FlagsData.php
Normal file
15
lib/Client/Protocol/Response/Line/Data/FlagsData.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/Client/Protocol/Response/Line/Data/ListData.php
Normal file
18
lib/Client/Protocol/Response/Line/Data/ListData.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/Client/Protocol/Response/Line/Data/RecentData.php
Normal file
12
lib/Client/Protocol/Response/Line/Data/RecentData.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Response\Line\Data;
|
||||||
|
|
||||||
|
final class RecentData implements Data
|
||||||
|
{
|
||||||
|
public function __construct(public int $numberOfMessages)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/Client/Protocol/Response/Line/Data/SearchData.php
Normal file
15
lib/Client/Protocol/Response/Line/Data/SearchData.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/Client/Protocol/Response/Line/Line.php
Normal file
9
lib/Client/Protocol/Response/Line/Line.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Response\Line;
|
||||||
|
|
||||||
|
interface Line
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/Client/Protocol/Response/Line/Status/Code/Code.php
Normal file
9
lib/Client/Protocol/Response/Line/Status/Code/Code.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||||
|
|
||||||
|
interface Code
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||||
|
|
||||||
|
final readonly class ReadOnlyCode implements Code
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Protocol\Response\Line\Status\Code;
|
||||||
|
|
||||||
|
final readonly class ReadWriteCode implements Code
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
13
lib/Client/Protocol/Response/Line/Status/Code/UnseenCode.php
Normal file
13
lib/Client/Protocol/Response/Line/Status/Code/UnseenCode.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/Client/Protocol/Response/Line/Status/Status.php
Normal file
19
lib/Client/Protocol/Response/Line/Status/Status.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/Client/Protocol/Response/Line/Status/StatusType.php
Normal file
12
lib/Client/Protocol/Response/Line/Status/StatusType.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?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';
|
||||||
|
}
|
||||||
79
lib/Client/Protocol/Response/Parser/Lexer.php
Normal file
79
lib/Client/Protocol/Response/Parser/Lexer.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/Client/Protocol/Response/Parser/ParseError.php
Normal file
24
lib/Client/Protocol/Response/Parser/ParseError.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1017
lib/Client/Protocol/Response/Parser/Parser.php
Normal file
1017
lib/Client/Protocol/Response/Parser/Parser.php
Normal file
File diff suppressed because it is too large
Load Diff
56
lib/Client/Protocol/Response/Parser/TokenType.php
Normal file
56
lib/Client/Protocol/Response/Parser/TokenType.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
37
lib/Client/Protocol/Response/Response.php
Normal file
37
lib/Client/Protocol/Response/Response.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
lib/Client/Protocol/Response/ResponseBuilder.php
Normal file
50
lib/Client/Protocol/Response/ResponseBuilder.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
lib/Client/Protocol/ResponseHandler.php
Normal file
88
lib/Client/Protocol/ResponseHandler.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
public function __construct(private Parser $parser)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(string $statusTag, ResponseStream $stream, ContinuationHandler $continuationHandler): Response
|
||||||
|
{
|
||||||
|
$responseBuilder = new ResponseBuilder($statusTag);
|
||||||
|
|
||||||
|
do {
|
||||||
|
$raw = $stream->readLine();
|
||||||
|
while (preg_match('/\{(?<bytes>\d+)}\r\n$/', $raw, $matches)) {
|
||||||
|
$raw .= $stream->read((int) $matches['bytes']);
|
||||||
|
$raw .= $stream->readLine();
|
||||||
|
}
|
||||||
|
$line = $this->parser->parse($raw);
|
||||||
|
|
||||||
|
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 = $stream->readLine();
|
||||||
|
while (preg_match('/\{(?<bytes>\d+)}\r\n$/', $raw, $matches)) {
|
||||||
|
$raw .= $stream->read((int) $matches['bytes']);
|
||||||
|
$raw .= $stream->readLine();
|
||||||
|
}
|
||||||
|
$line = $this->parser->parse($raw);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/Client/Protocol/TagGenerator.php
Normal file
36
lib/Client/Protocol/TagGenerator.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\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;
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/Client/Protocol/UnexpectedContinuationHandler.php
Normal file
15
lib/Client/Protocol/UnexpectedContinuationHandler.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
83
lib/Client/Search.php
Normal file
83
lib/Client/Search.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP;
|
||||||
|
|
||||||
|
use BadMethodCallException;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Gricob\IMAP\Mime\Message;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\Search\All;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\Search\Before;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\Search\Criteria;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\Search\Header;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\Search\Not;
|
||||||
|
use Gricob\IMAP\Protocol\Command\Argument\Search\Since;
|
||||||
|
|
||||||
|
class Search
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var list<Criteria>
|
||||||
|
*/
|
||||||
|
private array $criteria;
|
||||||
|
|
||||||
|
private bool $not = false;
|
||||||
|
|
||||||
|
public function __construct(private readonly Client $client)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function header(string $fieldName, string $value = ''): self
|
||||||
|
{
|
||||||
|
$this->addCriteria(new Header($fieldName, $value));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function before(DateTimeInterface $date): self
|
||||||
|
{
|
||||||
|
$this->addCriteria(new Before($date));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function since(DateTimeInterface $date): self
|
||||||
|
{
|
||||||
|
$this->addCriteria(new Since($date));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function not(): self
|
||||||
|
{
|
||||||
|
$this->not = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<Message>
|
||||||
|
*/
|
||||||
|
public function get(?PreFetchOptions $preFetchOptions = null): array
|
||||||
|
{
|
||||||
|
if ($this->not) {
|
||||||
|
throw new BadMethodCallException('Not key requires to specify a search key to be applied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$criteria = empty($this->criteria)
|
||||||
|
? [new All()]
|
||||||
|
: $this->criteria;
|
||||||
|
|
||||||
|
return $this->client->doSearch($criteria, $preFetchOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addCriteria(Criteria $criteria): void
|
||||||
|
{
|
||||||
|
if ($this->not) {
|
||||||
|
$criteria = new Not($criteria);
|
||||||
|
$this->not = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->criteria[] = $criteria;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/Client/Transport/Connection.php
Normal file
26
lib/Client/Transport/Connection.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Transport;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
interface Connection
|
||||||
|
{
|
||||||
|
public function isOpen(): bool;
|
||||||
|
|
||||||
|
public function open(): void;
|
||||||
|
|
||||||
|
public function close(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function send(string $data): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function receive(): ResponseStream;
|
||||||
|
}
|
||||||
11
lib/Client/Transport/ConnectionFailed.php
Normal file
11
lib/Client/Transport/ConnectionFailed.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Transport;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class ConnectionFailed extends RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
12
lib/Client/Transport/ResponseStream.php
Normal file
12
lib/Client/Transport/ResponseStream.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Transport;
|
||||||
|
|
||||||
|
interface ResponseStream
|
||||||
|
{
|
||||||
|
public function read(int $bytes): string;
|
||||||
|
|
||||||
|
public function readLine(): string;
|
||||||
|
}
|
||||||
148
lib/Client/Transport/Socket/SocketConnection.php
Normal file
148
lib/Client/Transport/Socket/SocketConnection.php
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Transport\Socket;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Gricob\IMAP\Transport\Connection;
|
||||||
|
use Gricob\IMAP\Transport\ConnectionFailed;
|
||||||
|
use Gricob\IMAP\Transport\ResponseStream;
|
||||||
|
|
||||||
|
class SocketConnection implements Connection
|
||||||
|
{
|
||||||
|
private string $transport;
|
||||||
|
private string $host;
|
||||||
|
private int $port;
|
||||||
|
private float $timeout;
|
||||||
|
private bool $verifyPeer;
|
||||||
|
private bool $allowSelfSigned;
|
||||||
|
private bool $verifyPeerName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var resource|false
|
||||||
|
*/
|
||||||
|
private $stream = false;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $transport,
|
||||||
|
string $host,
|
||||||
|
int $port,
|
||||||
|
float $timeout,
|
||||||
|
bool $verifyPeer = true,
|
||||||
|
bool $verifyPeerName = true,
|
||||||
|
bool $allowSelfSigned = false,
|
||||||
|
) {
|
||||||
|
$this->port = $port;
|
||||||
|
$this->host = $host;
|
||||||
|
$this->transport = $transport;
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
$this->verifyPeer = $verifyPeer;
|
||||||
|
$this->verifyPeerName = $verifyPeerName;
|
||||||
|
$this->allowSelfSigned = $allowSelfSigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOpen(): bool
|
||||||
|
{
|
||||||
|
return false !== $this->stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function open(): void
|
||||||
|
{
|
||||||
|
if ($this->isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stream = @stream_socket_client(
|
||||||
|
sprintf('%s://%s:%s', $this->transport, $this->host, $this->port),
|
||||||
|
$errorCode,
|
||||||
|
$errorMessage,
|
||||||
|
$this->timeout,
|
||||||
|
context: stream_context_create([
|
||||||
|
'ssl' => [
|
||||||
|
'verify_peer' => $this->verifyPeer,
|
||||||
|
'verify_peer_name' => $this->verifyPeerName,
|
||||||
|
'allow_self_signed' => $this->allowSelfSigned,
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (false === $this->stream) {
|
||||||
|
throw new ConnectionFailed(
|
||||||
|
sprintf('SocketConnection failed [%s]: %s', $errorCode, $errorMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close(): void
|
||||||
|
{
|
||||||
|
if (!$this->stream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($this->stream);
|
||||||
|
|
||||||
|
$this->stream = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade an open plain-TCP socket to TLS in-place (STARTTLS patch).
|
||||||
|
*
|
||||||
|
* Must be called after the server has responded OK to a STARTTLS command.
|
||||||
|
*
|
||||||
|
* @throws ConnectionFailed
|
||||||
|
*/
|
||||||
|
public function upgradeTls(): void
|
||||||
|
{
|
||||||
|
if (!$this->stream) {
|
||||||
|
throw new ConnectionFailed('Cannot upgrade TLS: connection is not open');
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_context_set_option($this->stream, [
|
||||||
|
'ssl' => [
|
||||||
|
'peer_name' => $this->host,
|
||||||
|
'verify_peer' => $this->verifyPeer,
|
||||||
|
'verify_peer_name' => $this->verifyPeerName,
|
||||||
|
'allow_self_signed' => $this->allowSelfSigned,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
|
||||||
|
| STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
|
||||||
|
|
||||||
|
$result = @stream_socket_enable_crypto(
|
||||||
|
$this->stream,
|
||||||
|
true,
|
||||||
|
$cryptoMethod,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result !== true) {
|
||||||
|
$last = error_get_last();
|
||||||
|
$detail = $last['message'] ?? 'unknown error';
|
||||||
|
throw new ConnectionFailed('STARTTLS upgrade failed: ' . $detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(string $data): void
|
||||||
|
{
|
||||||
|
if (!$this->stream) {
|
||||||
|
throw new Exception('Unable to send data. SocketConnection is not open');
|
||||||
|
}
|
||||||
|
|
||||||
|
fwrite($this->stream, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function receive(): ResponseStream
|
||||||
|
{
|
||||||
|
if (!$this->stream) {
|
||||||
|
throw new Exception('Unable to receive data. SocketConnection is not open');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SocketResponseStream($this->stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
lib/Client/Transport/Socket/SocketResponseStream.php
Normal file
44
lib/Client/Transport/Socket/SocketResponseStream.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Transport\Socket;
|
||||||
|
|
||||||
|
use Gricob\IMAP\Transport\ResponseStream;
|
||||||
|
|
||||||
|
final class SocketResponseStream implements ResponseStream
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param resource $stream
|
||||||
|
*/
|
||||||
|
public function __construct(private $stream)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(int $bytes): string
|
||||||
|
{
|
||||||
|
if ($bytes <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$remainingBytes = $bytes;
|
||||||
|
$data = '';
|
||||||
|
do {
|
||||||
|
$data .= fread($this->stream, $remainingBytes);
|
||||||
|
$remainingBytes = $bytes - strlen($data);
|
||||||
|
} while ($remainingBytes > 0);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readLine(): string
|
||||||
|
{
|
||||||
|
$line = '';
|
||||||
|
|
||||||
|
while ("\n" !== ($char = fread($this->stream, 1))) {
|
||||||
|
$line .= $char;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $line."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/Client/Transport/Traceable/TraceableConnection.php
Normal file
55
lib/Client/Transport/Traceable/TraceableConnection.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Transport\Traceable;
|
||||||
|
|
||||||
|
use Gricob\IMAP\Transport\Connection;
|
||||||
|
use Gricob\IMAP\Transport\ResponseStream;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
final readonly class TraceableConnection implements Connection
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Connection $connection,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOpen(): bool
|
||||||
|
{
|
||||||
|
return $this->connection->isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function open(): void
|
||||||
|
{
|
||||||
|
$this->connection->open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close(): void
|
||||||
|
{
|
||||||
|
$this->connection->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(string $data): void
|
||||||
|
{
|
||||||
|
$this->debug(addslashes($data));
|
||||||
|
|
||||||
|
$this->connection->send($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function receive(): ResponseStream
|
||||||
|
{
|
||||||
|
return new TraceableResponseStream(
|
||||||
|
$this->connection->receive(),
|
||||||
|
$this->logger,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function debug(string $data): void
|
||||||
|
{
|
||||||
|
$data = str_replace("\r\n", "\\r\\n", $data);
|
||||||
|
|
||||||
|
$this->logger->debug($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
lib/Client/Transport/Traceable/TraceableResponseStream.php
Normal file
43
lib/Client/Transport/Traceable/TraceableResponseStream.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Gricob\IMAP\Transport\Traceable;
|
||||||
|
|
||||||
|
use Gricob\IMAP\Transport\ResponseStream;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
final readonly class TraceableResponseStream implements ResponseStream
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ResponseStream $responseStream,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(int $bytes): string
|
||||||
|
{
|
||||||
|
$data = $this->responseStream->read($bytes);
|
||||||
|
|
||||||
|
$this->debug($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readLine(): string
|
||||||
|
{
|
||||||
|
$line = $this->responseStream->readLine();
|
||||||
|
|
||||||
|
$this->debug($line);
|
||||||
|
|
||||||
|
return $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function debug(string $data): void
|
||||||
|
{
|
||||||
|
$data = addslashes($data);
|
||||||
|
$data = str_replace("\r\n", "\\r\\n", $data);
|
||||||
|
|
||||||
|
$this->logger->debug($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
202
lib/Console/ServiceConnectCommand.php
Normal file
202
lib/Console/ServiceConnectCommand.php
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace KTXM\ProviderImapMail\Console;
|
||||||
|
|
||||||
|
use KTXM\ProviderImapMail\Providers\Provider;
|
||||||
|
use KTXM\ProviderImapMail\Providers\Service;
|
||||||
|
use KTXM\ProviderImapMail\Providers\ServiceIdentityBasic;
|
||||||
|
use KTXM\ProviderImapMail\Providers\ServiceLocation;
|
||||||
|
use KTXC\SessionTenant;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual IMAP service connection wizard.
|
||||||
|
*
|
||||||
|
* Interactively prompts for all connection details (host, port, encryption,
|
||||||
|
* username, password), runs a live connection test, then optionally persists
|
||||||
|
* the service to the store.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* bin/console provider_imap_mail:service:connect
|
||||||
|
* bin/console provider_imap_mail:service:connect --tenant=t1 --user=u1
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'provider_imap_mail:service:connect',
|
||||||
|
description: 'Manually configure and connect an IMAP service',
|
||||||
|
)]
|
||||||
|
class ServiceConnectCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Provider $provider,
|
||||||
|
private readonly SessionTenant $sessionTenant,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->addOption('tenant', 't', InputOption::VALUE_REQUIRED, 'Tenant ID')
|
||||||
|
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User ID')
|
||||||
|
->addOption('host', null, InputOption::VALUE_REQUIRED, 'IMAP server hostname')
|
||||||
|
->addOption('port', null, InputOption::VALUE_REQUIRED, 'IMAP port (default: 993)')
|
||||||
|
->addOption('encryption', null, InputOption::VALUE_REQUIRED, 'Encryption: ssl | starttls | none (default: ssl)')
|
||||||
|
->addOption('username', null, InputOption::VALUE_REQUIRED, 'IMAP username / e-mail')
|
||||||
|
->addOption('no-verify', null, InputOption::VALUE_NONE, 'Disable TLS certificate verification')
|
||||||
|
->addOption('no-save', null, InputOption::VALUE_NONE, 'Test connection only; do not persist')
|
||||||
|
->setHelp(<<<'HELP'
|
||||||
|
The <info>provider_imap_mail:service:connect</info> command walks you through
|
||||||
|
manually configuring an IMAP account. All prompts can be pre-filled via
|
||||||
|
options to support non-interactive / scripted usage.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
Fully interactive:
|
||||||
|
<info>bin/console provider_imap_mail:service:connect</info>
|
||||||
|
|
||||||
|
Pre-fill common options:
|
||||||
|
<info>bin/console provider_imap_mail:service:connect \
|
||||||
|
--host=mail.example.com --username=user@example.com \
|
||||||
|
--tenant=t1 --user=u1</info>
|
||||||
|
HELP);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$io->title('IMAP Service — Manual Configuration');
|
||||||
|
|
||||||
|
if (!$input->getOption('tenant')) {
|
||||||
|
$tenant = $io->ask('Tenant ID');
|
||||||
|
if ($tenant) $input->setOption('tenant', $tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$input->getOption('user')) {
|
||||||
|
$user = $io->ask('User ID');
|
||||||
|
if ($user) $input->setOption('user', $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$input->getOption('host')) {
|
||||||
|
$host = $io->ask('IMAP server hostname');
|
||||||
|
if ($host) $input->setOption('host', $host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$input->getOption('port')) {
|
||||||
|
$port = $io->ask('Port', '993');
|
||||||
|
if ($port) $input->setOption('port', $port);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$input->getOption('encryption')) {
|
||||||
|
$enc = $io->choice('Encryption', ['ssl', 'starttls', 'none'], 'ssl');
|
||||||
|
$input->setOption('encryption', $enc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$input->getOption('username')) {
|
||||||
|
$username = $io->ask('Username / e-mail address');
|
||||||
|
if ($username) $input->setOption('username', $username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$tenantId = (string) ($input->getOption('tenant') ?? '');
|
||||||
|
$userId = (string) ($input->getOption('user') ?? '');
|
||||||
|
$host = (string) ($input->getOption('host') ?? '');
|
||||||
|
$port = (int) ($input->getOption('port') ?? 993);
|
||||||
|
$encryption = (string) ($input->getOption('encryption') ?? 'ssl');
|
||||||
|
$username = (string) ($input->getOption('username') ?? '');
|
||||||
|
$noVerify = (bool) $input->getOption('no-verify');
|
||||||
|
$noSave = (bool) $input->getOption('no-save');
|
||||||
|
|
||||||
|
// ── Validate required fields ─────────────────────────────────────────
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
if ($host === '') $errors[] = 'Hostname is required.';
|
||||||
|
if ($username === '') $errors[] = 'Username is required.';
|
||||||
|
if (!$noSave) {
|
||||||
|
if ($tenantId === '') $errors[] = 'Tenant ID is required (or pass --no-save to test only).';
|
||||||
|
if ($userId === '') $errors[] = 'User ID is required (or pass --no-save to test only).';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
$io->error($errors);
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Prompt for password (always interactive — never passed via option) ─
|
||||||
|
|
||||||
|
$password = $io->askHidden('Password');
|
||||||
|
|
||||||
|
if (!$password) {
|
||||||
|
$io->error('Password is required.');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build service object ────────────────────────────────────────────
|
||||||
|
|
||||||
|
$location = new ServiceLocation(
|
||||||
|
host: $host,
|
||||||
|
port: $port > 0 ? $port : 993,
|
||||||
|
encryption: $encryption,
|
||||||
|
verifyPeer: !$noVerify,
|
||||||
|
verifyPeerName: !$noVerify,
|
||||||
|
allowSelfSigned: $noVerify,
|
||||||
|
);
|
||||||
|
|
||||||
|
$identity = (new ServiceIdentityBasic())->jsonDeserialize([
|
||||||
|
'identity' => $username,
|
||||||
|
'secret' => $password,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new Service();
|
||||||
|
$service->setLocation($location);
|
||||||
|
$service->setIdentity($identity);
|
||||||
|
|
||||||
|
// ── Test connection ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$io->text('Testing connection to <info>' . $host . ':' . $port . '</info>…');
|
||||||
|
|
||||||
|
$result = $this->provider->serviceTest($service);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
$io->error('Connection test failed: ' . $result['message']);
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->success($result['message']);
|
||||||
|
|
||||||
|
if ($noSave) {
|
||||||
|
$io->note('Connection test passed. Service not saved (--no-save).');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Persist ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$this->sessionTenant->configureById($tenantId);
|
||||||
|
|
||||||
|
$label = $io->ask('Service label', $username);
|
||||||
|
if ($label) {
|
||||||
|
$service->setLabel($label);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $this->provider->serviceCreate($tenantId, $userId, $service);
|
||||||
|
$io->success("Service saved with ID: <info>{$id}</info>");
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user