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; } }