rafalmasiarek / mailer
From-scratch SMTP client + MIME builder (no PHPMailer dependency) with native PGP/MIME body support and structured PSR-3 SMTP diagnostics.
Requires
- php: >=8.2
- psr/clock: ^1.0
- psr/log: ^3.0
- rafalmasiarek/dns-resolver: ^0.2.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-11 20:39:03 UTC
README
PHP mailer: from-scratch SMTP client + MIME builder (no PHPMailer), native PGP/MIME, structured PSR-3 SMTP diagnostics
Index
Classes
DkimSigner—rafalmasiarek\MailerDsnOptions—rafalmasiarek\MailerFailedDelivery—rafalmasiarek\MailerMimeBuilder—rafalmasiarek\MailerPgpMimeBodyBuilder—rafalmasiarek\MailerRawMimeBody—rafalmasiarek\MailerSmtpClient—rafalmasiarek\MailerSmtpDebugCapture—rafalmasiarek\MailerSmtpErrorCode—rafalmasiarek\MailerSmtpException—rafalmasiarek\MailerSystemClock—rafalmasiarek\MailerTlsOptions—rafalmasiarek\Mailer
Interfaces
DeadLetterStoreInterface—rafalmasiarek\Mailer
DeadLetterStoreInterface
interface — rafalmasiarek\Mailer\DeadLetterStoreInterface · source
Receives a FailedDelivery after SmtpClient::dispatch() exhausts its own error handling. Implement to persist failed sends for inspection or retry — SmtpClient has no built-in retry of its own, and still throws SmtpException after calling push().
| Method | Visibility | Returns | Summary |
|---|---|---|---|
push() |
public | void |
— |
push()
DeadLetterStoreInterface push()
public function push(FailedDelivery $delivery): void
| Param | Type | Description |
|---|---|---|
$delivery |
FailedDelivery |
— |
DkimSigner
final class — rafalmasiarek\Mailer\DkimSigner · source
Signs a complete RFC 5322 message with DKIM (RFC 6376), relaxed/relaxed canonicalization, rsa-sha256. Operates on the raw message string produced by MimeBuilder::build() — no knowledge of SMTP or transport; the returned string is prepended with a DKIM-Signature header and otherwise identical, ready to pass to SmtpClient as-is.
| Method | Visibility | Returns | Summary |
|---|---|---|---|
sign() |
public static | string |
— |
splitMessage() |
private static | array{headerBlock: string, body: string} |
— |
parseHeaders() |
private static | list<array{name: string, value: string}> |
Parses an unfolded header block into an ordered list of [name, value] pairs, reassembling folded continuation lines into the owning header's value. |
findHeader() |
private static | array{string, string}|null |
— |
canonicalizeHeaderRelaxed() |
private static | string |
RFC 6376 §3.4.2 relaxed header canonicalization: lowercase the name, unfold continuation lines, collapse whitespace runs to a single space, trim the value. |
canonicalizeBodyRelaxed() |
private static | string |
RFC 6376 §3.4.4 relaxed body canonicalization: collapse whitespace runs within each line, strip trailing whitespace per line, remove trailing blank lines, end with exactly one CRLF unless the body is empty. |
sign()
DkimSigner sign()
public static function sign(string $rawMessage, string $privateKeyPem, string $domain, string $selector, array $headersToSign): string
| Param | Type | Description |
|---|---|---|
$rawMessage |
string |
Complete message from MimeBuilder::build(). |
$privateKeyPem |
string |
PEM-encoded RSA private key. |
$domain |
string |
Signing domain (d= tag). |
$selector |
string |
DKIM selector (s= tag). |
$headersToSign |
list<string> |
Header field names to sign, in signing order. |
Returns: string — The message with a DKIM-Signature header prepended.
| Throws | When |
|---|---|
SmtpException |
When the private key is invalid or signing fails. |
splitMessage()
DkimSigner splitMessage()
private static function splitMessage(string $rawMessage): array
| Param | Type | Description |
|---|---|---|
$rawMessage |
string |
— |
Returns: array{headerBlock: string, body: string} — [headerBlock, body]
parseHeaders()
DkimSigner parseHeaders()
private static function parseHeaders(string $headerBlock): array
Parses an unfolded header block into an ordered list of [name, value] pairs, reassembling folded continuation lines into the owning header's value.
| Param | Type | Description |
|---|---|---|
$headerBlock |
string |
— |
Returns: list<array{name: string, value: string}>
findHeader()
DkimSigner findHeader()
private static function findHeader(array $headers, string $name): ?array
| Param | Type | Description |
|---|---|---|
$headers |
list<array{string, string}> |
— |
$name |
string |
— |
Returns: array{string, string}\|null
canonicalizeHeaderRelaxed()
DkimSigner canonicalizeHeaderRelaxed()
private static function canonicalizeHeaderRelaxed(string $name, string $value): string
RFC 6376 §3.4.2 relaxed header canonicalization: lowercase the name, unfold continuation lines, collapse whitespace runs to a single space, trim the value.
| Param | Type | Description |
|---|---|---|
$name |
string |
— |
$value |
string |
— |
Returns: string
canonicalizeBodyRelaxed()
DkimSigner canonicalizeBodyRelaxed()
private static function canonicalizeBodyRelaxed(string $body): string
RFC 6376 §3.4.4 relaxed body canonicalization: collapse whitespace runs within each line, strip trailing whitespace per line, remove trailing blank lines, end with exactly one CRLF unless the body is empty.
| Param | Type | Description |
|---|---|---|
$body |
string |
— |
Returns: string
DsnOptions
final class — rafalmasiarek\Mailer\DsnOptions · source
Delivery Status Notification request (RFC 3461). Applied to the MAIL FROM/RCPT TO commands only when the server's EHLO response advertises the DSN extension — silently omitted otherwise.
| Method | Visibility | Returns | Summary |
|---|---|---|---|
__construct() |
public | — | — |
__construct()
DsnOptions __construct()
public function __construct(string $ret, array $notify, ?string $envId)
| Param | Type | Description |
|---|---|---|
$ret |
'HDRS'|'FULL' |
How much of the original message a DSN report should include. |
$notify |
list<'SUCCESS'|'FAILURE'|'DELAY'|'NEVER'> |
Events to request a DSN report for. |
$envId |
string|null |
Opaque id echoed back as Original-Envelope-Id in any DSN report; must be xtext-safe (printable ASCII, no "+" or "="). |
FailedDelivery
final class — rafalmasiarek\Mailer\FailedDelivery · source
Snapshot of one failed SmtpClient::dispatch() call, handed to a DeadLetterStoreInterface so the message is not lost.
Depends on: \DateTimeImmutable
| Method | Visibility | Returns | Summary |
|---|---|---|---|
__construct() |
public | — | — |
__construct()
FailedDelivery __construct()
public function __construct(string $envelopeFrom, array $envelopeRecipients, string $rawMessage, string $error, \DateTimeImmutable $failedAt)
| Param | Type | Description |
|---|---|---|
$envelopeFrom |
string |
MAIL FROM address. |
$envelopeRecipients |
list<string> |
RCPT TO addresses. |
$rawMessage |
string |
Complete message from MimeBuilder::build(). |
$error |
string |
The exception message that caused the failure. |
$failedAt |
\DateTimeImmutable |
When the failure occurred. |
MimeBuilder
final class — rafalmasiarek\Mailer\MimeBuilder · source
Builds a complete RFC 5322 message (headers + body) ready to send as SMTP DATA content. Body nesting, innermost to outermost: resolveBody() (text/plain, text/html, or multipart/alternative, or a verbatim RawMimeBody) is wrapped in multipart/related when inline-embedded parts are present, which is in turn wrapped in multipart/mixed when attachments are present. Matches the RFC 2046 §5.1 recommended nesting order (mixed > related > alternative). Header values are passed through PHP's own mb_encode_mimeheader() (RFC 2047) so non-ASCII subjects/display names degrade safely rather than corrupting the message.
| Method | Visibility | Returns | Summary |
|---|---|---|---|
build() |
public static | string |
— |
resolveBody() |
private static | array{contentType: string, encoding: string, body: string} |
Resolves the top-level content-type/encoding/body before any related/mixed wrapping. |
wrapInRelated() |
private static | string |
Wraps the already-resolved body plus inline parts in multipart/related. |
wrapInMixed() |
private static | string |
Wraps the already-resolved body plus attachments in multipart/mixed. |
chunkBase64() |
private static | string |
Base64-encodes content and splits it into RFC-compliant 76-char lines. |
formatAddress() |
private static | string |
Formats a "Name " address header value, encoding the name if needed. |
encodeHeader() |
private static | string |
Encodes a header value per RFC 2047 when it contains non-ASCII bytes. |
boundary() |
private static | string |
Generates a unique MIME boundary string. |
guessMimeType() |
private static | string |
Best-effort MIME type guess from a file extension; falls back to a generic binary type when unknown. |
build()
MimeBuilder build()
public static function build(string $fromEmail, string $fromName, string $toEmail, string $toName, ?string $replyTo, string $subject, ?string $htmlBody, ?string $textBody, array $attachments, ?RawMimeBody $rawBody, string $messageId, array $cc, array $embeds, array $customHeaders, ?string $inReplyTo, array $references): string
| Param | Type | Description |
|---|---|---|
$fromEmail |
string |
Sender address. |
$fromName |
string |
Sender display name (may be empty). |
$toEmail |
string |
Recipient address. |
$toName |
string |
Recipient display name (may be empty). |
$replyTo |
string|null |
Reply-To address, or null. |
$subject |
string |
Subject line. |
$htmlBody |
string|null |
HTML body, or null when not used. |
$textBody |
string|null |
Plain-text body, or null when not used. |
$attachments |
list<array{path: string, name?: string, mimeType?: string}> |
Files to attach. |
$rawBody |
RawMimeBody|null |
Verbatim body, takes precedence over html/text when set. |
$messageId |
string |
Value for the Message-ID header (without angle brackets). |
$cc |
list<array{email: string, name?: string}> |
Carbon-copy recipients (header only — envelope recipients are a transport concern, not built here). |
$embeds |
list<array{path: string, cid: string, name?: string, mimeType?: string}> |
Inline parts referenced from the HTML body as "cid:...". Ignored when $rawBody is set. |
$customHeaders |
array<string, string> |
Additional raw header lines, name => value. |
$inReplyTo |
string|null |
Message-ID this message replies to (without angle brackets). |
$references |
list<string> |
Message-IDs for the References header (without angle brackets). |
Returns: string — Complete message: headers, blank line, body.
| Throws | When |
|---|---|
SmtpException |
When an attachment/embed path is not readable, or a custom header value contains embedded line breaks. |
resolveBody()
MimeBuilder resolveBody()
private static function resolveBody(?string $htmlBody, ?string $textBody, ?RawMimeBody $rawBody): array
Resolves the top-level content-type/encoding/body before any related/mixed wrapping.
| Param | Type | Description |
|---|---|---|
$htmlBody |
string|null |
— |
$textBody |
string|null |
— |
$rawBody |
RawMimeBody|null |
— |
Returns: array{contentType: string, encoding: string, body: string} — [contentType, encoding, body]
wrapInRelated()
MimeBuilder wrapInRelated()
private static function wrapInRelated(string $boundary, string $innerContentType, string $innerEncoding, string $innerBody, array $embeds): string
Wraps the already-resolved body plus inline parts in multipart/related.
| Param | Type | Description |
|---|---|---|
$boundary |
string |
— |
$innerContentType |
string |
— |
$innerEncoding |
string |
— |
$innerBody |
string |
— |
$embeds |
list<array{path: string, cid: string, name?: string, mimeType?: string}> |
— |
Returns: string
| Throws | When |
|---|---|
SmtpException |
When an embed path is not readable. |
wrapInMixed()
MimeBuilder wrapInMixed()
private static function wrapInMixed(string $boundary, string $innerContentType, string $innerEncoding, string $innerBody, array $attachments): string
Wraps the already-resolved body plus attachments in multipart/mixed.
| Param | Type | Description |
|---|---|---|
$boundary |
string |
— |
$innerContentType |
string |
— |
$innerEncoding |
string |
— |
$innerBody |
string |
— |
$attachments |
list<array{path: string, name?: string, mimeType?: string}> |
— |
Returns: string
| Throws | When |
|---|---|
SmtpException |
When an attachment path is not readable. |
chunkBase64()
MimeBuilder chunkBase64()
private static function chunkBase64(string $data): string
Base64-encodes content and splits it into RFC-compliant 76-char lines.
| Param | Type | Description |
|---|---|---|
$data |
string |
— |
Returns: string — Base64 content, chunked, CRLF-terminated.
formatAddress()
MimeBuilder formatAddress()
private static function formatAddress(string $email, string $name): string
Formats a "Name " address header value, encoding the name if needed.
| Param | Type | Description |
|---|---|---|
$email |
string |
— |
$name |
string |
— |
Returns: string
encodeHeader()
MimeBuilder encodeHeader()
private static function encodeHeader(string $value): string
Encodes a header value per RFC 2047 when it contains non-ASCII bytes.
| Param | Type | Description |
|---|---|---|
$value |
string |
— |
Returns: string
boundary()
MimeBuilder boundary()
private static function boundary(string $label): string
Generates a unique MIME boundary string.
| Param | Type | Description |
|---|---|---|
$label |
string |
— |
Returns: string
guessMimeType()
MimeBuilder guessMimeType()
private static function guessMimeType(string $path): string
Best-effort MIME type guess from a file extension; falls back to a generic binary type when unknown.
| Param | Type | Description |
|---|---|---|
$path |
string |
— |
Returns: string
PgpMimeBodyBuilder
final class — rafalmasiarek\Mailer\PgpMimeBodyBuilder · source
Builds a multipart/encrypted (RFC 3156, PGP/MIME) body from an already PGP-armored ciphertext block. Standalone — has no dependency on MailMessage/Mailer/anything else in this library. Feed the returned RawMimeBody into whatever "send this exact body verbatim" mechanism the caller's higher-level mail API provides.
| Method | Visibility | Returns | Summary |
|---|---|---|---|
build() |
public static | RawMimeBody |
Builds the raw multipart/encrypted body for an armored PGP MESSAGE block. |
normalizeCrlf() |
private static | string |
Normalises line endings to CRLF, as required by MIME specs. |
build()
PgpMimeBodyBuilder build()
public static function build(string $armoredCiphertext): RawMimeBody
Builds the raw multipart/encrypted body for an armored PGP MESSAGE block.
| Param | Type | Description |
|---|---|---|
$armoredCiphertext |
string |
Armored "-----BEGIN PGP MESSAGE-----" block. |
Returns: RawMimeBody — Body/content-type/encoding to send verbatim.
normalizeCrlf()
PgpMimeBodyBuilder normalizeCrlf()
private static function normalizeCrlf(string $s): string
Normalises line endings to CRLF, as required by MIME specs.
| Param | Type | Description |
|---|---|---|
$s |
string |
Input string with any line endings. |
Returns: string — String with all line endings replaced by CRLF.
RawMimeBody
final readonly class — rafalmasiarek\Mailer\RawMimeBody · source
A fully-formed MIME body to send verbatim, bypassing normal alternative/mixed construction.
| Method | Visibility | Returns | Summary |
|---|---|---|---|
__construct() |
public | — | — |
__construct()
RawMimeBody __construct()
public function __construct(string $body, string $contentType, string $encoding)
| Param | Type | Description |
|---|---|---|
$body |
string |
Raw MIME body content. |
$contentType |
string |
Content-Type header value for this body (may include boundary=…). |
$encoding |
string |
Content-Transfer-Encoding value. |
SmtpClient
final class — rafalmasiarek\Mailer\SmtpClient · source
From-scratch SMTP client (RFC 5321) — no PHPMailer dependency. Resolves the target host through an injected DnsResolverInterface (so DNS follows the same resolver chain as the rest of the app) and connects directly to the resolved IP, passing the original hostname via the stream context's ssl.peer_name option for correct TLS certificate/SNI verification. Supports STARTTLS and implicit TLS, AUTH PLAIN/LOGIN, and an optional debug callback receiving "C: ..."/"S: ..." transcript lines (pair with SmtpDebugCapture for AUTH redaction + structured logging). open()/dispatch()/close() allow sending multiple messages over one connection (session reuse); send() is a single-message convenience wrapper around the three. $maxPerSecond throttles between dispatch() calls within a session; $restartThreshold reconnects automatically after N dispatches, for servers that rate-limit or drop long-lived connections. DsnOptions requests a Delivery Status Notification (RFC 3461) per dispatch() call, applied only when the server's EHLO advertises DSN. $deadLetterStore, when set, receives a FailedDelivery on any dispatch() failure — SmtpException is still thrown either way. $tls controls certificate verification and an optional client certificate for mutual TLS.
Depends on: DnsResolverInterface, LoggerInterface, DeadLetterStoreInterface, TlsOptions, ClockInterface
| Property | Type | Visibility | Description |
|---|---|---|---|
$connection |
resource|null |
private | — |
$debugCallback |
callable(string): |
private | void|null |
$dispatchCount |
int |
private | — |
$lastDispatchAt |
float |
private | — |
$openHost |
string |
private | — |
$openPort |
int |
private | — |
$openEncryption |
string |
private | — |
$openUsername |
string |
private | — |
$openPassword |
string |
private | — |
$capabilities |
list<string> |
private | EHLO capability lines from the most recent open(), uppercase. |
| Method | Visibility | Returns | Summary |
|---|---|---|---|
__construct() |
public | — | — |
setDebugCallback() |
public | void |
Sets the debug callback invoked with each raw transcript line ("C: ..." for client-sent, "S: ..." for server lines). |
send() |
public | void |
Sends one message over a fresh connection, then disconnects. Convenience wrapper around open()/dispatch()/close() for callers that don't need session reuse. |
open() |
public | void |
Opens a connection and authenticates, ready for one or more dispatch() calls. |
dispatch() |
public | void |
Sends one message over the already-open connection. Applies the configured $maxPerSecond throttle before sending, and reconnects automatically afterward once $restartThreshold dispatches have been reached on this connection. |
close() |
public | void |
Sends QUIT and closes the connection opened by open(). Best-effort — never throws. |
throttle() |
private | void |
Sleeps as needed so dispatch() calls within a session respect $maxPerSecond. |
pushToDeadLetterStore() |
private | void |
Best-effort push to the configured DeadLetterStoreInterface. A store failure must never mask the real SmtpException about to be thrown. |
restart() |
private | void |
Closes and reopens the connection with the same credentials used by the last open() call. |
connect() |
private | void |
Resolves the host via DnsResolverInterface and opens the socket. |
enableCrypto() |
private | void |
Upgrades the current connection to TLS after STARTTLS. |
readGreeting() |
private | void |
Reads and validates the server's initial 220 greeting. |
ehlo() |
private | list<string> |
Sends EHLO and returns the server's advertised capability lines. |
authenticate() |
private | void |
Authenticates using AUTH PLAIN when the server advertises it, else AUTH LOGIN. |
sendData() |
private | void |
Sends the DATA command and the message body with dot-stuffing. |
quit() |
private | void |
Sends QUIT and closes the connection. Best-effort — never throws. |
command() |
private | string |
Sends one command and validates the response code. Rejects embedded CR/LF in the command — prevents SMTP command/header injection if a caller-supplied value (address, subject) ever contained a newline. |
write() |
private | void |
Writes raw bytes to the connection. |
readResponse() |
private | array{code: int, text: string} |
Reads one (possibly multiline) SMTP response. Continuation lines are "CODE-text\r\n"; the final line is "CODE text\r\n" (4th byte is a space, or end of line). Accumulates until the terminator — never assumes one line is the whole response. Retries stream_select() on EINTR (a signal interrupting the call) instead of treating it as a read failure. |
readLine() |
private | string |
Reads a single line (up to MAX_LINE_LENGTH bytes), retrying on EINTR. |
mustConnection() |
private | resource |
— |
debug() |
private | void |
— |
__construct()
SmtpClient __construct()
public function __construct(DnsResolverInterface $dns, LoggerInterface $logger, float $timeout, float $maxPerSecond, int $restartThreshold, ?DeadLetterStoreInterface $deadLetterStore, TlsOptions $tls, ClockInterface $clock)
| Param | Type | Description |
|---|---|---|
$dns |
DnsResolverInterface |
Resolver used to pin the connection's target IP. |
$logger |
LoggerInterface|null |
PSR-3 logger; NullLogger when omitted. |
$timeout |
float |
Socket read/connect timeout in seconds. |
$maxPerSecond |
float |
Maximum dispatch() calls per second within a session; 0 disables throttling. |
$restartThreshold |
int |
Reconnect automatically after this many dispatch() calls; 0 disables restarting. |
$deadLetterStore |
DeadLetterStoreInterface|null |
Receives a FailedDelivery when dispatch() fails; SmtpException is still thrown either way. |
$tls |
TlsOptions |
TLS verification/client-certificate behavior; defaults (verify against the system CA store, no client certificate) when omitted. Applies to both implicit TLS and STARTTLS. |
$clock |
ClockInterface |
Timestamps FailedDelivery::$failedAt; system clock when omitted. |
setDebugCallback()
SmtpClient setDebugCallback()
public function setDebugCallback(callable $callback): void
Sets the debug callback invoked with each raw transcript line ("C: ..." for client-sent, "S: ..." for server lines).
| Param | Type | Description |
|---|---|---|
$callback |
callable |
— |
send()
SmtpClient send()
public function send(string $host, int $port, string $encryption, string $username, string $password, string $envelopeFrom, array $envelopeRecipients, string $rawMessage, ?DsnOptions $dsn): void
Sends one message over a fresh connection, then disconnects. Convenience wrapper around open()/dispatch()/close() for callers that don't need session reuse.
| Param | Type | Description |
|---|---|---|
$host |
string |
SMTP server hostname. |
$port |
int |
SMTP server port. |
$encryption |
string |
'tls' (STARTTLS), 'ssl' (implicit TLS), or '' (none). |
$username |
string |
SMTP username, or '' to skip authentication. |
$password |
string |
SMTP password. |
$envelopeFrom |
string |
Envelope sender address (MAIL FROM). |
$envelopeRecipients |
list<string> |
Envelope recipient addresses (RCPT TO) — to, cc, and bcc addresses combined; headers are built separately by MimeBuilder and never derived from this list. |
$rawMessage |
string |
Complete RFC 5322 message (headers + body) from MimeBuilder. |
$dsn |
DsnOptions|null |
Delivery Status Notification request; ignored when the server doesn't advertise the DSN extension. |
| Throws | When |
|---|---|
SmtpException |
On any protocol or transport failure. |
open()
SmtpClient open()
public function open(string $host, int $port, string $encryption, string $username, string $password): void
Opens a connection and authenticates, ready for one or more dispatch() calls.
| Param | Type | Description |
|---|---|---|
$host |
string |
SMTP server hostname. |
$port |
int |
SMTP server port. |
$encryption |
string |
'tls' (STARTTLS), 'ssl' (implicit TLS), or '' (none). |
$username |
string |
SMTP username, or '' to skip authentication. |
$password |
string |
SMTP password. |
| Throws | When |
|---|---|
SmtpException |
On any protocol or transport failure. |
dispatch()
SmtpClient dispatch()
public function dispatch(string $envelopeFrom, array $envelopeRecipients, string $rawMessage, ?DsnOptions $dsn): void
Sends one message over the already-open connection. Applies the configured $maxPerSecond throttle before sending, and reconnects automatically afterward once $restartThreshold dispatches have been reached on this connection.
| Param | Type | Description |
|---|---|---|
$envelopeFrom |
string |
Envelope sender address (MAIL FROM). |
$envelopeRecipients |
list<string> |
Envelope recipient addresses (RCPT TO). |
$rawMessage |
string |
Complete RFC 5322 message (headers + body) from MimeBuilder. |
$dsn |
DsnOptions|null |
Delivery Status Notification request; ignored when the server doesn't advertise the DSN extension. |
| Throws | When |
|---|---|
SmtpException |
On any protocol or transport failure. |
close()
SmtpClient close()
public function close(): void
Sends QUIT and closes the connection opened by open(). Best-effort — never throws.
throttle()
SmtpClient throttle()
private function throttle(): void
Sleeps as needed so dispatch() calls within a session respect $maxPerSecond.
pushToDeadLetterStore()
SmtpClient pushToDeadLetterStore()
private function pushToDeadLetterStore(string $envelopeFrom, array $envelopeRecipients, string $rawMessage, string $error): void
Best-effort push to the configured DeadLetterStoreInterface. A store failure must never mask the real SmtpException about to be thrown.
| Param | Type | Description |
|---|---|---|
$envelopeFrom |
string |
— |
$envelopeRecipients |
list<string> |
— |
$rawMessage |
string |
— |
$error |
string |
— |
restart()
SmtpClient restart()
private function restart(): void
Closes and reopens the connection with the same credentials used by the last open() call.
| Throws | When |
|---|---|
SmtpException |
On any protocol or transport failure. |
connect()
SmtpClient connect()
private function connect(string $host, int $port, bool $implicitTls): void
Resolves the host via DnsResolverInterface and opens the socket.
| Param | Type | Description |
|---|---|---|
$host |
string |
— |
$port |
int |
— |
$implicitTls |
bool |
— |
| Throws | When |
|---|---|
SmtpException |
— |
enableCrypto()
SmtpClient enableCrypto()
private function enableCrypto(string $host): void
Upgrades the current connection to TLS after STARTTLS.
| Param | Type | Description |
|---|---|---|
$host |
string |
Original hostname, for SNI (already set on the stream context at connect time). |
| Throws | When |
|---|---|
SmtpException |
— |
readGreeting()
SmtpClient readGreeting()
private function readGreeting(): void
Reads and validates the server's initial 220 greeting.
| Throws | When |
|---|---|
SmtpException |
— |
ehlo()
SmtpClient ehlo()
private function ehlo(string $host): array
Sends EHLO and returns the server's advertised capability lines.
| Param | Type | Description |
|---|---|---|
$host |
string |
— |
Returns: list<string> — Capability lines, uppercase.
| Throws | When |
|---|---|
SmtpException |
— |
authenticate()
SmtpClient authenticate()
private function authenticate(string $username, string $password, array $capabilities): void
Authenticates using AUTH PLAIN when the server advertises it, else AUTH LOGIN.
| Param | Type | Description |
|---|---|---|
$username |
string |
— |
$password |
string |
— |
$capabilities |
list<string> |
EHLO capability lines. |
| Throws | When |
|---|---|
SmtpException |
— |
sendData()
SmtpClient sendData()
private function sendData(string $rawMessage): void
Sends the DATA command and the message body with dot-stuffing.
| Param | Type | Description |
|---|---|---|
$rawMessage |
string |
— |
| Throws | When |
|---|---|
SmtpException |
— |
quit()
SmtpClient quit()
private function quit(): void
Sends QUIT and closes the connection. Best-effort — never throws.
command()
SmtpClient command()
private function command(string $command, array $expect): string
Sends one command and validates the response code. Rejects embedded CR/LF in the command — prevents SMTP command/header injection if a caller-supplied value (address, subject) ever contained a newline.
| Param | Type | Description |
|---|---|---|
$command |
string |
— |
$expect |
list<int> |
Acceptable response codes. |
Returns: string — The full (possibly multiline) response text.
| Throws | When |
|---|---|
SmtpException |
— |
write()
SmtpClient write()
private function write(string $data): void
Writes raw bytes to the connection.
| Param | Type | Description |
|---|---|---|
$data |
string |
— |
| Throws | When |
|---|---|
SmtpException |
— |
readResponse()
SmtpClient readResponse()
private function readResponse(): array
Reads one (possibly multiline) SMTP response. Continuation lines are "CODE-text\r\n"; the final line is "CODE text\r\n" (4th byte is a space, or end of line). Accumulates until the terminator — never assumes one line is the whole response. Retries stream_select() on EINTR (a signal interrupting the call) instead of treating it as a read failure.
Returns: array{code: int, text: string}
| Throws | When |
|---|---|
SmtpException |
— |
readLine()
SmtpClient readLine()
private function readLine( $connection): string
Reads a single line (up to MAX_LINE_LENGTH bytes), retrying on EINTR.
| Param | Type | Description |
|---|---|---|
$connection |
resource |
— |
Returns: string
| Throws | When |
|---|---|
SmtpException |
— |
mustConnection()
SmtpClient mustConnection()
private function mustConnection():
Returns: resource
| Throws | When |
|---|---|
SmtpException |
— |
debug()
SmtpClient debug()
private function debug(string $line): void
| Param | Type | Description |
|---|---|---|
$line |
string |
— |
SmtpDebugCapture
final class — rafalmasiarek\Mailer\SmtpDebugCapture · source
Captures the raw SMTP transcript and redacts AUTH credential exchange. Pass the callable from getCallback() to SmtpClient's debug hook. Expects SmtpClient's own transcript line format: "C: " for client-sent lines, "S: " for server lines. AUTH filtering strategy: - the "C: AUTH ..." line is replaced with a redacted marker. - all subsequent lines (challenge/response base64 exchange) are dropped. - capture resumes after the server's 235 (success) or 535 (failure) response. - the 235/535 line itself is kept so the auth outcome is visible.
Depends on: ClockInterface
| Property | Type | Visibility | Description |
|---|---|---|---|
$lines |
list<string> |
private | — |
$inAuthExchange |
bool |
private | Whether we are currently inside an AUTH credential exchange. |
| Method | Visibility | Returns | Summary |
|---|---|---|---|
__construct() |
public | — | — |
getCallback() |
public | callable(string): |
Returns the callable to pass to SmtpClient's debug hook. |
hasLines() |
public | bool |
Returns true when at least one SMTP debug line was captured. |
getLines() |
public | list<string> |
Returns all captured lines. |
writeToFile() |
public | void |
Writes the captured transcript to a file, creating parent directories as needed. |
append() |
private | void |
Prepends a timestamp and stores a line. |
__construct()
SmtpDebugCapture __construct()
public function __construct(ClockInterface $clock)
| Param | Type | Description |
|---|---|---|
$clock |
ClockInterface |
Clock used for log line timestamps. |
getCallback()
SmtpDebugCapture getCallback()
public function getCallback(): callable
Returns the callable to pass to SmtpClient's debug hook.
Returns: callable(string): — void
hasLines()
SmtpDebugCapture hasLines()
public function hasLines(): bool
Returns true when at least one SMTP debug line was captured.
Returns: bool
getLines()
SmtpDebugCapture getLines()
public function getLines(): array
Returns all captured lines.
Returns: list<string>
writeToFile()
SmtpDebugCapture writeToFile()
public function writeToFile(string $path): void
Writes the captured transcript to a file, creating parent directories as needed.
| Param | Type | Description |
|---|---|---|
$path |
string |
Absolute path to the target log file. |
append()
SmtpDebugCapture append()
private function append(string $line): void
Prepends a timestamp and stores a line.
| Param | Type | Description |
|---|---|---|
$line |
string |
— |
SmtpErrorCode
final class — rafalmasiarek\Mailer\SmtpErrorCode · source
Canonical SMTP error codes and classifier. All needle-matching logic lives here so every caller (logging, API error responses, …) derives the same structured reason code from a raw SMTP error string.
| Method | Visibility | Returns | Summary |
|---|---|---|---|
message() |
public static | string |
Returns the human-readable message for an ERR_SMTP_* code. |
classify() |
public static | string |
Classifies an SMTP error string into one of the ERR_SMTP_* constants. Checks for an RFC 3463 enhanced status code first — Postfix and most modern MTAs include one on nearly every rejection, and it is far more reliable than matching human-readable text that varies between server software. Falls back to free-text needles for connection-level failures that never reach the SMTP protocol layer, where no such code exists. |
message()
SmtpErrorCode message()
public static function message(string $code): string
Returns the human-readable message for an ERR_SMTP_* code.
| Param | Type | Description |
|---|---|---|
$code |
string |
One of the ERR_SMTP_* constant values. |
Returns: string — Human-readable error description.
classify()
SmtpErrorCode classify()
public static function classify(string $errorInfo, string $exceptionMessage): string
Classifies an SMTP error string into one of the ERR_SMTP_* constants. Checks for an RFC 3463 enhanced status code first — Postfix and most modern MTAs include one on nearly every rejection, and it is far more reliable than matching human-readable text that varies between server software. Falls back to free-text needles for connection-level failures that never reach the SMTP protocol layer, where no such code exists.
| Param | Type | Description |
|---|---|---|
$errorInfo |
string |
Raw SMTP server response / error text. |
$exceptionMessage |
string |
Exception::getMessage() for the same failure. |
Returns: string — One of the ERR_SMTP_* constant values.
SmtpException
class — rafalmasiarek\Mailer\SmtpException · source
Thrown when an SMTP operation fails.
extends \RuntimeException
SystemClock
final class — rafalmasiarek\Mailer\SystemClock · source
Default PSR-20 clock backed by the system time.
implements ClockInterface
| Method | Visibility | Returns | Summary |
|---|---|---|---|
now() |
public | \DateTimeImmutable |
{@inheritDoc} |
now()
SystemClock now()
public function now(): \DateTimeImmutable
{@inheritDoc}
Returns: \DateTimeImmutable
TlsOptions
final class — rafalmasiarek\Mailer\TlsOptions · source
TLS behavior for SmtpClient's connection (implicit TLS and STARTTLS alike).
| Method | Visibility | Returns | Summary |
|---|---|---|---|
__construct() |
public | — | — |
__construct()
TlsOptions __construct()
public function __construct(bool $verifyPeer, ?string $caFile, ?string $clientCertFile, ?string $clientKeyFile, ?string $clientKeyPassphrase)
| Param | Type | Description |
|---|---|---|
$verifyPeer |
bool |
Verify the server's certificate chain and hostname against the system CA store. Set false only to trust any certificate unconditionally — prefer $caFile to pin a specific self-signed certificate or private CA instead. |
$caFile |
string|null |
PEM file containing the specific certificate (or CA) to trust, used instead of the system CA store. Verification still runs when $verifyPeer is true — this only changes what counts as trusted. |
$clientCertFile |
string|null |
PEM file with the client certificate, for servers requiring mutual TLS. |
$clientKeyFile |
string|null |
PEM file with the client private key; same file as $clientCertFile when both are stored together. |
$clientKeyPassphrase |
string|null |
Passphrase for $clientKeyFile, when encrypted. |