refactor: use custom imap client

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-05-08 00:16:43 -04:00
parent a728aeb11c
commit a8764747fd
179 changed files with 6782 additions and 5907 deletions

View File

@@ -1,26 +0,0 @@
<?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;
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace KTXM\ProviderImap\Client\Transport;
use KTXM\ProviderImap\Client\ConnectionConfig;
use Psr\Log\LoggerInterface;
interface ConnectionFactoryInterface
{
public function create(ConnectionConfig $config, ?LoggerInterface $logger = null): ConnectionInterface;
}

View File

@@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace Gricob\IMAP\Transport;
use RuntimeException;
class ConnectionFailed extends RuntimeException
{
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace KTXM\ProviderImap\Client\Transport;
use KTXM\ProviderImap\Client\ConnectionConfig;
interface ConnectionInterface
{
public function connect(ConnectionConfig $config): void;
public function disconnect(): void;
public function isConnected(): bool;
public function write(string $payload): void;
public function readLine(): string;
public function readBytes(int $length): string;
public function upgradeToTls(): void;
}

View File

@@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace Gricob\IMAP\Transport;
interface ResponseStream
{
public function read(int $bytes): string;
public function readLine(): string;
}

View File

@@ -1,148 +0,0 @@
<?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);
}
}

View File

@@ -1,44 +0,0 @@
<?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";
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace KTXM\ProviderImap\Client\Transport;
use KTXM\ProviderImap\Client\ConnectionConfig;
use KTXM\ProviderImap\Client\ImapException;
use Psr\Log\LoggerInterface;
final class SocketConnection implements ConnectionInterface
{
/** @var resource|null */
private $stream = null;
public function __construct(
private readonly ConnectionConfig $config,
private readonly ?LoggerInterface $logger = null,
) {}
public function connect(ConnectionConfig $config): void
{
if ($this->isConnected()) {
return;
}
$context = stream_context_create($config->streamContextOptions());
$errorCode = 0;
$errorMessage = '';
$stream = @stream_socket_client(
$config->endpoint(),
$errorCode,
$errorMessage,
$config->timeout(),
STREAM_CLIENT_CONNECT,
$context,
);
if (!is_resource($stream)) {
$this->logger?->error('IMAP socket connection failed', [
'endpoint' => $config->endpoint(),
'code' => $errorCode,
'message' => $errorMessage ?: 'unknown error',
]);
throw new ImapException(sprintf(
'Unable to connect to %s (%d: %s)',
$config->endpoint(),
$errorCode,
$errorMessage ?: 'unknown error',
));
}
stream_set_timeout($stream, (int) $config->timeout());
$this->stream = $stream;
$this->logger?->info('IMAP socket connected to {endpoint} (timeout={timeout})', [
'endpoint' => $config->endpoint(),
'timeout' => $config->timeout(),
]);
}
public function disconnect(): void
{
if (!is_resource($this->stream)) {
return;
}
fclose($this->stream);
$this->stream = null;
$this->logger?->info('IMAP socket disconnected');
}
public function isConnected(): bool
{
return is_resource($this->stream);
}
public function write(string $payload): void
{
$stream = $this->stream();
$written = fwrite($stream, $payload);
if ($written === false || $written !== strlen($payload)) {
$this->logger?->error('IMAP socket write failed (bytes={bytes})', [
'bytes' => strlen($payload),
]);
throw new ImapException('Failed to write complete payload to IMAP socket.');
}
}
public function readLine(): string
{
$stream = $this->stream();
$line = fgets($stream);
if ($line === false) {
$this->logger?->error('IMAP socket read failed');
throw new ImapException('Failed to read line from IMAP socket.');
}
return $line;
}
public function readBytes(int $length): string
{
if ($length < 0) {
throw new ImapException('IMAP socket cannot read a negative number of bytes.');
}
if ($length === 0) {
return '';
}
$stream = $this->stream();
$buffer = '';
while (strlen($buffer) < $length) {
$chunk = fread($stream, $length - strlen($buffer));
if ($chunk === false || $chunk === '') {
$this->logger?->error('IMAP socket literal read failed (bytes={bytes})', [
'bytes' => $length,
]);
throw new ImapException('Failed to read literal payload from IMAP socket.');
}
$buffer .= $chunk;
}
return $buffer;
}
public function upgradeToTls(): void
{
$stream = $this->stream();
$result = stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
if ($result !== true) {
$this->logger?->error('IMAP TLS upgrade failed');
throw new ImapException('Failed to enable TLS on IMAP socket.');
}
$this->logger?->info('IMAP socket upgraded to TLS');
}
/**
* @return resource
*/
private function stream()
{
if (!is_resource($this->stream)) {
throw new ImapException('IMAP socket is not connected.');
}
return $this->stream;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace KTXM\ProviderImap\Client\Transport;
use KTXM\ProviderImap\Client\ConnectionConfig;
use Psr\Log\LoggerInterface;
final class SocketConnectionFactory implements ConnectionFactoryInterface
{
public function create(ConnectionConfig $config, ?LoggerInterface $logger = null): ConnectionInterface
{
return new SocketConnection($config, $logger);
}
}

View File

@@ -1,55 +0,0 @@
<?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);
}
}

View File

@@ -1,43 +0,0 @@
<?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);
}
}