Files
provider_imap/lib/Client/Transport/SocketConnection.php
2026-05-23 20:18:58 -04:00

182 lines
4.8 KiB
PHP

<?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 readBytesChunked(int $length, int $chunkSize = 8192): \Generator
{
if ($length < 0) {
throw new ImapException('IMAP socket cannot read a negative number of bytes.');
}
$remaining = $length;
while ($remaining > 0) {
$chunk = fread($this->stream(), min($chunkSize, $remaining));
if ($chunk === false || $chunk === '') {
throw new ImapException('Failed to read literal payload from IMAP socket.');
}
$remaining -= strlen($chunk);
yield $chunk;
}
}
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;
}
}