API / Minn\Mail

Minn\Mail

Sending mail and the notices the engine sends. 24 classes, 94 public methods and 39 internals. This page is the public surface; each class name opens the whole class.

AddressRules final class

public/minn/src/Minn/Mail/AddressRules.php · 91 lines · Open the class →

Email addresses: the checks a mailer can apply (PHP's filter, the RFC 5322 grammar with comments, quoted local parts and domain literals, the HTML5 form rule), parsing an address list, and the domain of an address in ASCII.

staticvalid(string $address, string $pattern): bool

Whether the address passes the named check ("php", "pcre", "pcre8", "html5", "noregex").

staticparseList(string $list, string $charset): array

The addresses in a comma-separated list, as the reference's simple parser reads one: split at every comma, quoted or not, each entry with its display name (encoded words decoded into $charset); entries that are not valid addresses are left out.

@return list<array{name: string, address: string}>
staticasciiDomain(string $address, string $charset): string

The address with its domain in ASCII (IDNA), the domain read from $charset first; unchanged when it cannot be.

staticquoted(string $text): string

The text as an RFC 822 quoted string when it holds a character that needs one.

insiderfc5322()

ChangeNotices final class

public/minn/src/Minn/Mail/ChangeNotices.php · 30 lines · Open the class →

The notices the reference mails a user when their password or email address changes, as templates: plugins filter them (password_change_email, email_change_email) with the placeholders still in them and the site name still a %s in the subject, and fill() finishes them after.

staticpassword(): array

The password notice: subject and message, placeholders unfilled. @return array{subject: string, message: string}

@return array{subject: string, message: string}
staticemail(): array

The email-address notice, sent to the address being left: subject and message, placeholders unfilled. @return array{subject: string, message: string}

@return array{subject: string, message: string}
staticfill(string $text, array $values): string

A template's placeholders filled with their values. @param array<string, string> $values placeholder name (USERNAME, ...) => value

@param array<string, string> $values placeholder name (USERNAME, ...) => value

Composer final class

public/minn/src/Minn/Mail/Composer.php · 253 lines · Open the class →

A message as the reference's mailer writes it: the header block (Date, the address lines, Subject, Message-ID, X-Mailer, custom headers, MIME-Version), the MIME header for its type, and the body, multipart when it has an alternative, inline parts or attachments. Boundaries are "b1=_{id}" to "b3=_{id}"; a 7-bit-clean body declared 8bit goes out as 7bit us-ascii, and a body with a line past 998 characters as quoted-printable.

staticbody(Minn\Mail\Draft $d, string $id): array

The body and the encoding a single-part message declares.

@return array{body: string, encoding: string}
staticheaders(Minn\Mail\Draft $d, string $id, string $hostname, string $encoding): string

The header block, MIME-Version and the MIME header included.

staticmimeHeaders(Minn\Mail\Draft $d, string $id, string $encoding): string

The MIME header for the message's type: the content type (with its boundary) and the transfer encoding.

staticmailOnlyHeaders(Minn\Mail\Draft $d): string

The To and Subject lines PHP's mail() takes as arguments, written after the headers in the sent copy.

staticaddressLine(Minn\Mail\Draft $d, string $name, array $list): string

"Name: a, b" for a list of [address, name] pairs.

@param list<array{0: string, 1: string}> $list
staticaddress(Minn\Mail\Draft $d, array $pair): string

One address as a header writes it: the address alone, or the name (as a phrase) and the address in angle brackets.

@param array{0: string, 1: string} $pair
staticsecure(string $text): string

Header text with any line break taken out.

staticmessageId(string $given, string $id, string $hostname): string

The message id given when it is well formed, else one made from the id and the host name.

insideencodingFor(), partHead(), related(), alternative(), end(), calendar(), date(), attachAll(), attachmentHead(), attachmentData(), extraLines()

DataLines final class

public/minn/src/Minn/Mail/DataLines.php · 28 lines · Open the class →

A message cut into the lines SMTP DATA sends. Any line break (CRLF, CR or LF) ends a line. A line longer than 998 characters is cut at its last space within the first 998 (the space dropped), or at 997 when there is none; in the header block (when the first line reads "Name: ..." with no space in the name, up to the first empty line) each continuation starts with a tab. Dot stuffing is the sender's job, after the cut.

staticsplit(string $message): array

The lines to send, without their line endings.

@return list<string>

DebugOutput final class

public/minn/src/Minn/Mail/DebugOutput.php · 38 lines · Open the class →

Where a mailer's debug text goes, as its Debugoutput setting names it: "echo" (a timestamp and a tab, continuation lines indented under the text), "html" (entity-escaped, line breaks dropped, ending in <br>), "error_log", a PSR-3 logger's debug(), or any other callable, which gets the text and its level. The two printed forms come back as text for the caller to print; the others are delivered here and return null.

staticsmtp(mixed $how, string $text, int $level): ?string

Debug text from the SMTP client: the text to print, or null once delivered; its html form carries a timestamp.

staticmailer(mixed $how, string $text, int $level): ?string

Debug text from the mailer itself: the text to print, or null once delivered; its html form has no timestamp.

insidewrite(), html()

Dkim final class

public/minn/src/Minn/Mail/Dkim.php · 110 lines · Open the class →

DKIM signatures (RFC 6376) as the reference's mailer writes them: rsa-sha256, relaxed header and simple body canonicalization, the standard headers signed in the order they appear (plus any extra ones the caller names), an optional identity, the signed headers copied into z= when asked, and the signature folded in 73-character pieces.

new__construct(string $domain, string $selector, string $key, string $passphrase, string $identity)
methodcopyingHeaders(): self

The same signer, copying the signed headers into z= as well.

staticquotedPrintable(string $text): string

Text with everything outside the DKIM-safe printable set written as =XX.

staticheaders(string $text): string

Relaxed header canonicalization: unfolded, names lower-cased, runs of space collapsed, values trimmed.

staticbody(string $body): string

Simple body canonicalization: CRLF line endings and exactly one at the end.

methodsign(string $text): ?string

The base64 RSA-SHA256 signature of the text, or null when the key cannot be read.

methodsignatureHeader(string $headerBlock, string $subject, string $body, int $time, array $extra, string $eol): ?string

The DKIM-Signature header for a message, signed at $time.

@param list<string> $extra lower-case names of further headers to sign
insidesignedHeaders()

Draft final readonly class

public/minn/src/Minn/Mail/Draft.php · 66 lines · Open the class →

Everything a message is composed from: the transport it goes out on (which decides the line ending, the header line length, and where To, Subject and Bcc are written), the envelope, the content, the headers, and the attachments as [path or data, file name, name, encoding, type, is data, disposition, content id]. $toHeader is "list" (To, or undisclosed-recipients when there is neither To nor Cc) or "omit" (one message per recipient, the address given to each send instead).

constVERSION = '7.1.1'
new__construct(string $mailer, string $eol, string $charset, string $contentType, string $encoding, string $subject, string $body, string $altBody, string $ical, string $from, string $fromName, array $to, array $cc, array $bcc, array $replyTo, string $messageId, string $messageDate, ?int $priority, string $xMailer, string $confirmReadingTo, array $customHeaders, array $attachments, string $toHeader = 'list')
@param list<array{0: string, 1: string}> $to address and name pairs, as are $cc, $bcc and $replyTo
@param list<array{0: string, 1: string}> $customHeaders name and value pairs
@param list<array<int, mixed>> $attachments
propreadonly string $mailer
propreadonly string $eol
propreadonly string $charset
propreadonly string $contentType
propreadonly string $encoding
propreadonly string $subject
propreadonly string $body
propreadonly string $altBody
propreadonly string $ical
propreadonly string $from
propreadonly string $fromName
propreadonly array $to
propreadonly array $cc
propreadonly array $bcc
propreadonly array $replyTo
propreadonly string $messageId
propreadonly string $messageDate
propreadonly ?int $priority
propreadonly string $xMailer
propreadonly string $confirmReadingTo
propreadonly array $customHeaders
propreadonly array $attachments
propreadonly string $toHeader
methodlineLength(): int

The longest header line the transport takes before it must fold.

methodencodeHeader(string $text, string $position = 'text'): string

Header text encoded for this message's charset and transport.

methodmessageType(): string

Which parts the message has: plain, or "alt", "inline", "attach" joined by "_".

HeaderWords final class

public/minn/src/Minn/Mail/HeaderWords.php · 112 lines · Open the class →

Header text as mail carries it (RFC 2047): left alone when it is plain and short enough, quoted when a phrase needs it, otherwise encoded words in Q (mostly ASCII) or B (mostly not), folded to the line length the transport allows: 63 for PHP's mail(), 998 otherwise. Plain text only becomes an encoded word to be folded when it is too long for one line.

constMAIL_LINE = 63
constSMTP_LINE = 998
staticencode(string $text, string $position, string $charset, int $lineLength, string $eol): string

The header text encoded for its position ("text", "phrase" or "comment").

staticq(string $text, string $position = 'text'): string

RFC 2047 "Q" encoding for a position: spaces as "_", and the characters the position cannot carry as =XX.

staticbase64Lines(string $text, string $charset): array

Base64 lines of at most 63 characters cut at whole characters, so a multibyte character is never split across encoded words.

@return list<string>
staticdecode(string $value, string $charset): string

Encoded words decoded (adjacent ones joined) and converted into $charset; other text passes through.

insidespecials(), bLines(), qLines()

HostEntry final readonly class

public/minn/src/Minn/Mail/HostEntry.php · 36 lines · Open the class →

One entry of a mailer's Host setting ("host", "host:port", "ssl://host:465", "tls://host"; entries are separated by semicolons): the security prefix, the host and the port. Only ssl:// and tls:// are prefixes; anything else stays part of the host and fails the host check.

new__construct(string $prefix, string $host, ?int $port)
propreadonly string $prefix
propreadonly string $host
propreadonly ?int $port
staticparseAll(string $hosts): array

The entries of a Host setting, null where one cannot be read.

@return list<array{0: string, 1: self|null}> the trimmed entry text and its reading
staticvalidHost(mixed $host): bool

Whether a host name, IPv4 address or bracketed IPv6 address is well formed.

HtmlMessage final class

public/minn/src/Minn/Mail/HtmlMessage.php · 55 lines · Open the class →

What a mailer reads out of an HTML message: the images it can carry inline (a src or background naming a file under the base directory, or a data: URI of a raster image), the content id each one gets (the first 32 hex digits of a SHA-256 of the URL, or of a data URI's bytes, at "phpmailer.0"), and the plain-text version (head, style and script dropped, tags stripped, entities decoded into the charset).

constNO_TEXT = 'This is an HTML-only message. To view it, activate HTML in your email application.'
staticimages(string $html): array

Every quoted src or background value, in document order.

@return list<array{attribute: string, url: string}>
staticdataImage(string $url): ?array

A data: URI's bytes and type when it is a raster image, else null.

@return array{0: string, 1: string}|null
staticlocalPath(string $url): ?string

A relative path with no scheme and no step up out of the base directory, or null.

staticcid(string $source): string

The content id the mailer gives an image.

statictext(string $html, string $charset): string

The plain-text version of an HTML message.

MailHeaders final readonly class

public/minn/src/Minn/Mail/MailHeaders.php · 83 lines · Open the class →

The headers argument of wp_mail() read the way the reference reads it: a string split into lines (CRLF or LF) or a list of lines; a line without a colon is skipped. From gives an address and a name (quotes dropped); Cc, Bcc and Reply-To add comma-separated entries (a comma inside quotes still splits); Content-Type gives a type and either a charset or a boundary (a boundary leaves the charset empty); every other header is kept by name, the last of a name winning.

new__construct(?string $fromEmail, ?string $fromName, array $cc, array $bcc, array $replyTo, ?string $contentType, ?string $charset, string $boundary, array $custom)
@param list<string> $cc
@param list<string> $bcc
@param list<string> $replyTo
@param array<string, string> $custom
propreadonly ?string $fromEmail
propreadonly ?string $fromName
propreadonly array $cc
propreadonly array $bcc
propreadonly array $replyTo
propreadonly ?string $contentType
propreadonly ?string $charset
propreadonly string $boundary
propreadonly array $custom
staticparse(mixed $headers): self

The headers argument, as a string of lines or a list of them.

staticrecipient(string $entry): array

One recipient entry as [address, name]: "Name <address>" (the name as written) or a bare address.

@return array{0: string, 1: string}
insidefrom(), contentType()

MailLog final class

public/minn/src/Minn/Mail/MailLog.php · 45 lines · Open the class →

The engine's development transport: each message as one JSON line (time, from, to, subject, body) appended to wp-content/minn-mail.log.php, nothing sent. The file opens with a line that returns from any request for it, so a web server that runs PHP never serves the reset links it holds (audit Low: it was a plain .log under the webroot).

constGUARD = '<?php return; ?> '

The first line of the file: PHP that returns before the rest is sent.

staticforSite(): string

The site's log file.

staticread(string $file): array

The messages a log file holds, oldest first (its guard line aside).

@return list<array<string, mixed>>
staticwrite(string $file, string $from, string $fromName, array $to, string $subject, string $body): bool

Appends one message; false when the file cannot be written.

@param list<string> $to

MailSettings final readonly class

public/minn/src/Minn/Mail/MailSettings.php · 71 lines · Open the class →

How mail leaves the site, from the minn_mail option (JSON or a serialized array): the PHP mail function by default, SMTP when configured, or a log file for development. The sender defaults to the site name at a no-reply address on the home host.

constOPTION = 'minn_mail'
new__construct(string $transport = 'mail', string $host = '', int $port = 587, string $encryption = 'tls', string $username = '', string $password = '', string $fromEmail = '', string $fromName = '')
propreadonly string $transport
propreadonly string $host
propreadonly int $port
propreadonly string $encryption
propreadonly string $username
propreadonly string $password
propreadonly string $fromEmail
propreadonly string $fromName
staticstored(Minn\Content\Site $site): array

What the minn_mail option holds, without defaults: empty when the site has not configured mail.

@return array<string, mixed>
staticfromSite(Minn\Content\Site $site): self

The site's mail settings from its minn_mail option, or the defaults.

methodtoArray(): array

The option's JSON, secrets included, for the settings surface.

Mailer final readonly class

public/minn/src/Minn/Mail/Mailer.php · 88 lines · Open the class →

Sends a Message through the configured transport. Failures are logged and reported as false; nothing here throws into a request.

new__construct(Minn\Mail\MailSettings $settings, string $logFile)
staticforSite(Minn\Content\Site $site): self

A mailer using the site's own settings.

staticmail(array|string $to, string $subject, string $body): bool

The one-line send: a recipient, a subject, a body, through the site's transport and sender, with nothing to construct at the call site.

staticnoticesFor(Minn\Content\Site $site): Minn\Mail\Notices

The engine's own notices, worded once, from this site's name and address.

methodsend(Minn\Mail\Message $message): bool

Sends one message; false on failure. With WordPress's mail function loaded the message goes through wp_mail(), so its filters and the phpmailer_init hook (an SMTP plugin's way in) apply as they do on the reference; otherwise through the configured transport directly.

insideviaSmtp(), viaMail(), draft()

MailerStrings final class

public/minn/src/Minn/Mail/MailerStrings.php · 34 lines · Open the class →

The mailer's English messages, keyed as plugins look them up ("authenticate", "provide_address", ...): the base set, and the set WordPress's own mailer subclass loads, which words four of them differently. Captured from the reference into data/mailer-strings.json.

staticbase(): array

The base mailer's messages.

@return array<string, string>
staticwordpress(): array

The messages WordPress's mailer subclass loads.

@return array<string, string>
insideset()

Message final readonly class

public/minn/src/Minn/Mail/Message.php · 18 lines · Open the class →

One outgoing plain-text email.

new__construct(array $to, string $subject, string $body, string $fromEmail = '', string $fromName = '')
@param list<string> $to
propreadonly array $to
propreadonly string $subject
propreadonly string $body
propreadonly string $fromEmail
propreadonly string $fromName
staticto(array|string $to, string $subject, string $body): self

One recipient or several, and the two things every mail has.

MimeTypes final class

public/minn/src/Minn/Mail/MimeTypes.php · 43 lines · Open the class →

The MIME type a mailer gives an attachment by its file extension (the table the reference's mailer answers with); anything else is application/octet-stream.

staticforExtension(string $extension): string

The type for an extension, in any case.

staticforFilename(string $filename): string

The type for a file name or path: its last extension (a query string ignored).

Notices final readonly class

public/minn/src/Minn/Mail/Notices.php · 99 lines · Open the class →

The messages the engine itself sends, worded as the reference words them and built in one place, so a controller and a CLI verb that announce the same thing say the same thing.

new__construct(string $siteName, string $home)
methodloginDetails(string $login, string $email, string $resetLink): Minn\Mail\Message

A new account's login details, with a link to choose a password.

methodpasswordReset(string $login, string $email, string $resetLink, string $requestIp): Minn\Mail\Message

The password reset link, with the requester's address as the reference prints it.

methodmoderation(string $adminEmail, string $postTitle, string $author, string $comment): Minn\Mail\Message

A comment waiting in the queue, announced to the site's address.

methodnewComment(string $authorEmail, string $postTitle, string $author, string $comment, string $link): Minn\Mail\Message

An approved comment on a post, announced to the post's author.

methodnewNote(string $authorEmail, string $postTitle, string $author, string $note, string $editLink): Minn\Mail\Message

A post's author told of a note on it, with where the post is edited (notes live in the editor).

insidesubject()

PathParts final class

public/minn/src/Minn/Mail/PathParts.php · 22 lines · Open the class →

A path's parts the way a mailer names attachments, safe for multibyte names: the directory ('' when there is none), the last segment, its extension (after the last dot, so ".hidden" is all extension) and the name before it. Either slash separates; trailing ones are ignored.

staticof(string $path): array

The path's four parts.

@return array{dirname: string, basename: string, extension: string, filename: string}

Smime final class

public/minn/src/Minn/Mail/Smime.php · 27 lines · Open the class →

S/MIME signing of a composed MIME entity with a certificate and key on disk: the entity goes through OpenSSL as a detached signature and comes back as new MIME headers (multipart/signed) and the signed body.

staticsign(string $entity, string $cert, string $key, string $passphrase, string $extraCerts): ?array

The signed entity's MIME headers and body, or null when signing fails.

@return array{0: string, 1: string}|null

Smtp final readonly class

public/minn/src/Minn/Mail/Smtp.php · 33 lines · Open the class →

Delivery over SMTP for the engine's own mail when WordPress's mail function is not loaded: the site's server, SSL or STARTTLS, a sign-in when a user name is set, one message per connection, through the same SmtpSession the PHPMailer SMTP class speaks with.

new__construct(Minn\Mail\MailSettings $settings)
methodsendRaw(string $from, array $recipients, string $data): bool

Sends one composed message (headers, a blank line, the body) to the envelope recipients; false on any refusal.

@param list<string> $recipients

SmtpSession final class

public/minn/src/Minn/Mail/SmtpSession.php · 461 lines · Open the class →

One SMTP conversation as the reference's client holds it: the socket, the last reply, the server's extensions from EHLO, and the last error as {error, detail, smtp_code, smtp_code_ex}. Every command names itself in its error ("RCPT TO command failed"); a command sent before connecting fails as "Called X without being connected". Debug text goes to the given closure with its level: 1 client lines, 2 server replies, 3 connection events, 4 raw inbound lines. Credentials never reach it.

constMAX_LINE = 998
new__construct(Closure $debug)
@param Closure(string, int): void $debug
methodlimits(int $timeout, int $timelimit): void

How long one read may wait and how long a whole reply may take, in seconds.

methodconnect(string $host, ?int $port, int $timeout, array $options): bool

Opens the connection and reads the greeting.

@param array<string, mixed> $options stream context options
methodstartTls(): bool

Asks for TLS and turns it on.

methodauthenticate(string $user, string $password, string $type, ?string $oauth): bool

Signs in with the named mechanism, or the strongest the server offers when it is empty or not offered.

methodconnected(): bool

Whether the connection is open; a socket closed at the far end is closed here too.

methodclose(): void

Closes the socket and forgets the server's greeting and extensions; the last error stays.

methoddata(string $message): bool

Sends a message: DATA, the lines (a line past 998 characters split at its last space, or at 997 without one; header continuations indented; leading dots doubled), then the closing dot.

methodhello(string $host): bool

EHLO, falling back to HELO; the extensions come from the EHLO reply.

methodmail(string $from, string $parameters): bool

MAIL FROM, with the given parameters (" XVERP", " SMTPUTF8") after the address.

methodquit(string $onError = 'close'): bool

QUIT; the connection closes when it succeeds, or anyway unless told to keep it.

methodrecipient(string $address, string $dsn): bool

RCPT TO, with the delivery notifications asked for (NEVER, SUCCESS, FAILURE, DELAY).

methodxclient(array $vars, array $allowed): bool

XCLIENT with the named attributes that are in $allowed; others are dropped.

@param array<string, string> $vars
@param list<string> $allowed
methodturn(): bool

TURN is refused here, as the reference refuses it.

methodcommand(string $name, string $line, array $expect): bool

Sends one command and reads the reply; a reply outside $expect fails with "{name} command failed" and the reply's detail and codes.

@param list<int> $expect
methodsend(string $data, string $command): int|false

Writes raw text to the server; credential commands show as hidden in the debug text.

methoderror(): array

The last error; every field is empty after a command that succeeded.

@return array{error: string, detail: string, smtp_code: int|string, smtp_code_ex: string}
methodextensions(): ?array

The extensions from the last EHLO (or the HELO greeting), null before one.

@return array<string, mixed>|null
methodextension(string $name): mixed

One extension's value: true for a bare one, a string or list for one with arguments, false when absent, null before EHLO.

methodlastReply(): string

The server's last reply, every line.

methodtransactionId(): string|false|null

The queue id the server gave the last message: null before one was sent, false when none was recognised.

staticparseReply(string $reply): array

The reply's code, enhanced code (when present) and the text after them.

@return array{0: int, 1: string, 2: string}
insidegreet(), extensionsFrom(), mechanism(), unsupported(), open(), lines(), failed(), setError(), say()

TextWrap final class

public/minn/src/Minn/Mail/TextWrap.php · 118 lines · Open the class →

Word wrapping for mail text. Lines break at spaces once they would pass the length; existing line breaks stay; a word longer than the line stays whole in plain text, and in quoted-printable text is cut into pieces ending in a soft break ("="), never inside an =XX escape or (for UTF-8) inside an encoded character. A line broken at a space keeps the space and, in quoted-printable text, a soft break after it.

staticplain(string $text, int $length, string $eol): string

Plain text wrapped to the length, every line ending in $eol.

staticquotedPrintable(string $text, int $length, string $charset, string $eol): string

Quoted-printable text wrapped to the length with soft breaks, every line ending in $eol.

staticutf8Boundary(string $encoded, int $max): int

Where to cut quoted-printable UTF-8 text at or before $max without splitting an encoded character.

insidewrapWith(), line(), closeLine(), pieces(), cut()

Transfer final class

public/minn/src/Minn/Mail/Transfer.php · 61 lines · Open the class →

Body text in a content transfer encoding, and the line-ending helpers mail composition leans on.

constMAX_LINE = 998
staticencode(string $text, string $encoding, string $eol): string

The text in an encoding: 7bit, 8bit and binary keep the text (the first two with normalized line breaks and a final one), base64 in 76-column lines, quoted-printable by PHP's encoder.

staticquotedPrintable(string $text, string $eol): string

Quoted-printable text with the encoder's line breaks normalized.

staticnormalizeBreaks(string $text, string $eol = ' '): string

Every CRLF, CR or LF as $eol.

staticstripTrailingSpace(string $text): string

The text without trailing spaces, tabs and line breaks.

staticstripTrailingBreaks(string $text): string

The text without trailing line breaks.

statichas8bit(string $text): bool

Whether any byte is outside 7-bit ASCII.

statichasLongLine(string $text): bool

Whether any line runs past the 998 characters SMTP allows.