refactor: smtp client

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-23 16:51:54 -04:00
parent ad6b7c5eba
commit 67a0dbbee7
6 changed files with 33 additions and 32 deletions
+1 -1
View File
@@ -644,7 +644,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
}
$this->initialize();
$smtp = $this->remoteService->submitClient();
$smtp = $this->remoteService->smtpClient();
try {
$queueId = $smtp->send(trim($sender->getAddress()), $recipients, $raw);
} finally {
+23 -23
View File
@@ -10,9 +10,9 @@ declare(strict_types=1);
namespace KTXM\ProviderImap\Smtp;
use KTXM\ProviderImap\Smtp\Auth\AuthMechanism;
use KTXM\ProviderImap\Smtp\Protocol\CommandWriter;
use KTXM\ProviderImap\Smtp\Protocol\Reply;
use KTXM\ProviderImap\Smtp\Protocol\ReplyReader;
use KTXM\ProviderImap\Smtp\Protocol\ProtocolWriter;
use KTXM\ProviderImap\Smtp\Protocol\Response\Response;
use KTXM\ProviderImap\Smtp\Protocol\ProtocolReader;
use KTXM\ProviderImap\Smtp\Transport\ConnectionFactoryInterface;
use KTXM\ProviderImap\Smtp\Transport\ConnectionInterface;
use KTXM\ProviderImap\Smtp\Transport\SocketConnectionFactory;
@@ -30,8 +30,8 @@ use Psr\Log\LoggerInterface;
final class Client
{
private ?ConnectionInterface $connection = null;
private ?ReplyReader $reader = null;
private ?CommandWriter $writer = null;
private ?ProtocolReader $reader = null;
private ?ProtocolWriter $writer = null;
private ?ConnectionConfig $config = null;
private ?SmtpCapabilities $capabilities = null;
@@ -47,8 +47,8 @@ final class Client
$this->connection = $connection;
$this->config = $config;
$this->reader = new ReplyReader($connection, $this->logger);
$this->writer = new CommandWriter($connection, $this->logger);
$this->reader = new ProtocolReader($connection, $this->logger);
$this->writer = new ProtocolWriter($connection, $this->logger);
// Server greeting.
$greeting = $this->reader()->read();
@@ -101,17 +101,17 @@ final class Client
}
$mailFrom .= ' SIZE=' . $size;
}
$this->command($mailFrom, static fn (Reply $r): bool => $r->isPositiveCompletion(), 'MAIL FROM');
$this->command($mailFrom, static fn (Response $r): bool => $r->isPositiveCompletion(), 'MAIL FROM');
foreach ($recipients as $recipient) {
$this->command(
sprintf('RCPT TO:<%s>', $recipient),
static fn (Reply $r): bool => $r->isPositiveCompletion(),
static fn (Response $r): bool => $r->isPositiveCompletion(),
'RCPT TO',
);
}
$this->command('DATA', static fn (Reply $r): bool => $r->isPositiveIntermediate(), 'DATA');
$this->command('DATA', static fn (Response $r): bool => $r->isPositiveIntermediate(), 'DATA');
$this->writer()->writeRaw($this->dotStuff($rawMessage) . "\r\n.\r\n");
$reply = $this->reader()->read();
@@ -161,7 +161,7 @@ final class Client
throw new SmtpException('SMTP server does not advertise STARTTLS.');
}
$this->command('STARTTLS', static fn (Reply $r): bool => $r->isPositiveCompletion(), 'STARTTLS');
$this->command('STARTTLS', static fn (Response $r): bool => $r->isPositiveCompletion(), 'STARTTLS');
$this->connection()->upgradeToTls();
}
@@ -185,7 +185,7 @@ final class Client
{
$this->command(
'AUTH PLAIN ' . AuthMechanism::plainToken($username, $password),
static fn (Reply $r): bool => $r->isPositiveCompletion(),
static fn (Response $r): bool => $r->isPositiveCompletion(),
'AUTH PLAIN',
true,
);
@@ -193,27 +193,27 @@ final class Client
private function authLogin(string $username, string $password): void
{
$this->command('AUTH LOGIN', static fn (Reply $r): bool => $r->isPositiveIntermediate(), 'AUTH LOGIN');
$this->command('AUTH LOGIN', static fn (Response $r): bool => $r->isPositiveIntermediate(), 'AUTH LOGIN');
$this->writer()->writeLine(base64_encode($username), true);
$userReply = $this->reader()->read();
if (!$userReply->isPositiveIntermediate()) {
throw new SmtpException('SMTP AUTH LOGIN username rejected: ' . $userReply->text());
$userResponse = $this->reader()->read();
if (!$userResponse->isPositiveIntermediate()) {
throw new SmtpException('SMTP AUTH LOGIN username rejected: ' . $userResponse->text());
}
$this->writer()->writeLine(base64_encode($password), true);
$passReply = $this->reader()->read();
if (!$passReply->isPositiveCompletion()) {
throw new SmtpException('SMTP authentication failed: ' . $passReply->text());
$passResponse = $this->reader()->read();
if (!$passResponse->isPositiveCompletion()) {
throw new SmtpException('SMTP authentication failed: ' . $passResponse->text());
}
}
/**
* Issue a command line and assert the reply satisfies $accept.
*
* @param callable(Reply):bool $accept
* @param callable(Response):bool $accept
*/
private function command(string $line, callable $accept, string $label, bool $sensitive = false): Reply
private function command(string $line, callable $accept, string $label, bool $sensitive = false): Response
{
$this->writer()->writeLine($line, $sensitive);
$reply = $this->reader()->read();
@@ -242,12 +242,12 @@ final class Client
return $this->connection ?? throw new SmtpException('SMTP client is not connected.');
}
private function reader(): ReplyReader
private function reader(): ProtocolReader
{
return $this->reader ?? throw new SmtpException('SMTP client is not connected.');
}
private function writer(): CommandWriter
private function writer(): ProtocolWriter
{
return $this->writer ?? throw new SmtpException('SMTP client is not connected.');
}
@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace KTXM\ProviderImap\Smtp\Protocol;
use KTXM\ProviderImap\Smtp\Protocol\Response\Response;
use KTXM\ProviderImap\Smtp\SmtpException;
use KTXM\ProviderImap\Smtp\Transport\ConnectionInterface;
use Psr\Log\LoggerInterface;
@@ -20,14 +21,14 @@ use Psr\Log\LoggerInterface;
* ("250-text") to indicate continuation and a space ("250 text") on the final
* line. See RFC 5321 §4.2.
*/
final class ReplyReader
final class ProtocolReader
{
public function __construct(
private readonly ConnectionInterface $connection,
private readonly ?LoggerInterface $logger = null,
) {}
public function read(): Reply
public function read(): Response
{
$lines = [];
$raw = '';
@@ -63,6 +64,6 @@ final class ReplyReader
'raw' => rtrim($raw, "\r\n"),
]);
return new Reply($code ?? 0, $lines, rtrim($raw, "\r\n"));
return new Response($code ?? 0, $lines, rtrim($raw, "\r\n"));
}
}
@@ -12,7 +12,7 @@ namespace KTXM\ProviderImap\Smtp\Protocol;
use KTXM\ProviderImap\Smtp\Transport\ConnectionInterface;
use Psr\Log\LoggerInterface;
final class CommandWriter
final class ProtocolWriter
{
public function __construct(
private readonly ConnectionInterface $connection,
@@ -7,12 +7,12 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImap\Smtp\Protocol;
namespace KTXM\ProviderImap\Smtp\Protocol\Response;
/**
* A parsed SMTP reply: a 3-digit status code plus one or more text lines.
*/
final class Reply
final class Response
{
/**
* @param int $code 3-digit SMTP status code
+2 -2
View File
@@ -9,7 +9,7 @@ declare(strict_types=1);
namespace KTXM\ProviderImap\Smtp;
use KTXM\ProviderImap\Smtp\Protocol\Reply;
use KTXM\ProviderImap\Smtp\Protocol\Response\Response;
/**
* Parsed EHLO capabilities.
@@ -27,7 +27,7 @@ final class SmtpCapabilities
private readonly array $keywords,
) {}
public static function fromEhlo(Reply $reply): self
public static function fromEhlo(Response $reply): self
{
$keywords = [];
$lines = $reply->lines();