* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderImap\Smtp\Protocol; use KTXM\ProviderImap\Smtp\SmtpException; use KTXM\ProviderImap\Smtp\Transport\ConnectionInterface; use Psr\Log\LoggerInterface; /** * Reads SMTP replies, assembling multi-line responses. * * A multi-line reply repeats the status code on every line, using a hyphen * ("250-text") to indicate continuation and a space ("250 text") on the final * line. See RFC 5321 ยง4.2. */ final class ReplyReader { public function __construct( private readonly ConnectionInterface $connection, private readonly ?LoggerInterface $logger = null, ) {} public function read(): Reply { $lines = []; $raw = ''; $code = null; while (true) { $rawLine = $this->connection->readLine(); $raw .= $rawLine; $trimmed = rtrim($rawLine, "\r\n"); if (!preg_match('/^(\d{3})([ -]?)(.*)$/', $trimmed, $matches)) { throw new SmtpException(sprintf('Malformed SMTP reply line: %s', $trimmed)); } $lineCode = (int) $matches[1]; $separator = $matches[2]; $lines[] = $matches[3]; if ($code === null) { $code = $lineCode; } elseif ($lineCode !== $code) { throw new SmtpException(sprintf('Inconsistent SMTP reply code: %s', $trimmed)); } // A space separator (or no separator) marks the final line. if ($separator !== '-') { break; } } $this->logger?->debug('SMTP reply received: {raw}', [ 'code' => $code, 'raw' => rtrim($raw, "\r\n"), ]); return new Reply($code ?? 0, $lines, rtrim($raw, "\r\n")); } }