generated from Nodarx/template
feat: smtp sending
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImap\Smtp\Protocol;
|
||||
|
||||
use KTXM\ProviderImap\Smtp\Transport\ConnectionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class CommandWriter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConnectionInterface $connection,
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Write a single command line, appending the CRLF terminator.
|
||||
*
|
||||
* @param bool $sensitive when true the payload is redacted from logs
|
||||
* (used for AUTH credential exchanges)
|
||||
*/
|
||||
public function writeLine(string $line, bool $sensitive = false): void
|
||||
{
|
||||
$this->logger?->debug('SMTP command sent: {command}', [
|
||||
'command' => $sensitive ? $this->redact($line) : $line,
|
||||
]);
|
||||
|
||||
$this->connection->write($line . "\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write raw bytes verbatim (e.g. the DATA payload). Never logged.
|
||||
*/
|
||||
public function writeRaw(string $payload): void
|
||||
{
|
||||
$this->connection->write($payload);
|
||||
}
|
||||
|
||||
private function redact(string $line): string
|
||||
{
|
||||
if (preg_match('/^(AUTH\s+\S+)(\s+.*)?$/i', $line, $matches) === 1) {
|
||||
return $matches[1] . (isset($matches[2]) ? ' [REDACTED]' : '');
|
||||
}
|
||||
|
||||
return '[REDACTED]';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImap\Smtp\Protocol;
|
||||
|
||||
/**
|
||||
* A parsed SMTP reply: a 3-digit status code plus one or more text lines.
|
||||
*/
|
||||
final class Reply
|
||||
{
|
||||
/**
|
||||
* @param int $code 3-digit SMTP status code
|
||||
* @param list<string> $lines text portion of each reply line (code/separator stripped)
|
||||
* @param string $raw the raw wire text (for diagnostics)
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly int $code,
|
||||
private readonly array $lines,
|
||||
private readonly string $raw,
|
||||
) {}
|
||||
|
||||
public function code(): int
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function lines(): array
|
||||
{
|
||||
return $this->lines;
|
||||
}
|
||||
|
||||
public function text(): string
|
||||
{
|
||||
return implode(' ', $this->lines);
|
||||
}
|
||||
|
||||
public function raw(): string
|
||||
{
|
||||
return $this->raw;
|
||||
}
|
||||
|
||||
/** 2xx — requested action completed. */
|
||||
public function isPositiveCompletion(): bool
|
||||
{
|
||||
return $this->code >= 200 && $this->code < 300;
|
||||
}
|
||||
|
||||
/** 3xx — more input required (e.g. DATA, AUTH challenge). */
|
||||
public function isPositiveIntermediate(): bool
|
||||
{
|
||||
return $this->code >= 300 && $this->code < 400;
|
||||
}
|
||||
|
||||
/** 4xx — transient negative completion (try again later). */
|
||||
public function isTransientError(): bool
|
||||
{
|
||||
return $this->code >= 400 && $this->code < 500;
|
||||
}
|
||||
|
||||
/** 5xx — permanent negative completion. */
|
||||
public function isPermanentError(): bool
|
||||
{
|
||||
return $this->code >= 500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user