* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderImap\Smtp\Transport; use KTXM\ProviderImap\Smtp\ConnectionConfig; use KTXM\ProviderImap\Smtp\SmtpException; use Psr\Log\LoggerInterface; final class SocketConnection implements ConnectionInterface { /** @var resource|null */ private $stream = null; public function __construct( 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('SMTP socket connection failed', [ 'endpoint' => $config->endpoint(), 'code' => $errorCode, 'message' => $errorMessage ?: 'unknown error', ]); throw new SmtpException(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('SMTP 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('SMTP 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('SMTP socket write failed (bytes={bytes})', [ 'bytes' => strlen($payload), ]); throw new SmtpException('Failed to write complete payload to SMTP socket.'); } } public function readLine(): string { $stream = $this->stream(); $line = fgets($stream); if ($line === false) { $this->logger?->error('SMTP socket read failed'); throw new SmtpException('Failed to read line from SMTP socket.'); } return $line; } 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('SMTP TLS upgrade failed'); throw new SmtpException('Failed to enable TLS on SMTP socket.'); } $this->logger?->info('SMTP socket upgraded to TLS'); } /** * @return resource */ private function stream() { if (!is_resource($this->stream)) { throw new SmtpException('SMTP socket is not connected.'); } return $this->stream; } }