Changelog
[Unreleased]
Changed
- BREAKING: the mailer configuration class is now
Myth\Postal\Config\Mailer(wasMyth\Postal\Config\Email), andMailerManagerresolves it by its short name. The old name collided with the framework's legacyConfig\Email, which forced resolution by fully-qualified class name — and that form never consults an application subclass, so the documented override path could not work. Applications that published aConfig\Emailextending the package config should rename it toConfig\Mailerand extendMyth\Postal\Config\Mailer. The framework's own legacyConfig\Email, used byLegacyEmailAdapterandspark email:test, is unaffected. See ADR 0001.
Added
-
spark publishwritesapp/Config/Mailer.php, aConfig\Mailerextending the package's config, which Postal then resolves in preference to its own default. Re-running publish never overwrites an existing file. -
Failover transport (
failovermailer): a composite that tries an ordered list of child mailers (named under achainkey) and falls through to the next on failure — returning the first success and reporting failure only when every child fails. A child that throws is treated as a failure so the chain advances.MailerManagerresolves the child mailers by name and hands the already-built transports to the composite. A failover mailer with no children throws aPostalException. - Automatic inline images: the renderer scans the HTML body for
<img>sources and embeds the embeddable ones —data:image URIs (decoded in memory) and local image file paths (read at render) — rewriting each reference to acid:URL and de-duplicating identical sources. Remote (http(s)://) and existingcid:sources are left untouched, and only files that sniff as images are read. On by default; disable per message withEmail::$autoEmbedImages = false. AddsAttachment::embedData(). - Amazon SES transport (
sesmailer): delivers through the officialaws/aws-sdk-phpSesV2 client (SigV4 signing and HTTP owned by the SDK). Sends structured Simple content by default and switches to raw MIME for attachments/inline images, HTML without an explicit text body, or whenforceRawis set. MapsEmail::metadata()onto SES EmailTags (dropping and debug-logging tags that break SES's character rules), applies an optionalconfigurationSet, and returns the SES message id.aws/aws-sdk-phpis an optional dependency (install it withcomposer require aws/aws-sdk-php). Email::metadata(key, value)builder for provider tags, mapped by API transports onto their tagging feature and ignored by the others.- Attachments and inline images:
Email::attach()(file by path, read lazily at render),Email::attachData()(raw bytes), andEmail::embedImage()(inline CID image referenceable from HTML), backed by theAttachmentvalue object. The renderer nestsmultipart/mixed(attachments) aroundmultipart/related(inline images) around the existingmultipart/alternative, with base64 parts chunked at 76 columns and part headers sanitised against filename injection. - Core send pipeline: compose an
Emailand send it through a transport viaservice('mailer'), receiving aSendResult. Emailmessage builder withfrom/replyTo/to/cc/bcc/subject/html/text/header/priority/returnPath(mutable, chainable).Addressvalue object that parses and renders"Name <email>".SendResultwithok()/fail()/cancelled()factories.TransportInterfaceand theNullTransportimplementation.MailerManager(resolves the default and named mailers fromConfig\Mailervia an extensible transport map, lazily and cached) and a minimalMailerthat clones the message at the dispatch boundary.Config\Mailerconfiguration and theservice('mailer')service.MessageRendererthat serialises anEmailinto a raw RFC 5322 / MIME string:text/plain, ormultipart/alternativewhenever HTML is present (with an automatically generated HTML→text fallback when no text body is set). Emits custom headers,Return-Path/Sender, and non-defaultX-Priority; applies RFC 2047 header encoding and quoted-printable body encoding (7-bit clean, within the 998-octet SMTP limit); strips CR/LF to prevent header injection; exposes the rendered header set viaheaders().LogTransportand the built-inlogmailer, which render the message and write the full MIME to a PSR-3 log channel (default leveldebug) instead of delivering it.- Email lifecycle events fired around every send:
email.composing(at the start),email.sending(immediately before the transport — returningfalsecancels the send and yieldsSendResult::cancelled()), andemail.sent/email.failedafterwards (each receiving theEmailandSendResult). All emission is gated behindConfig\Mailer::$fireEvents(defaulttrue). SendmailTransport/MailTransportand thesendmailandmailmailers, which hand the rendered MIME to a local MTA:sendmailpipes it to the configured binary (path) with-oi -t, andmailsplits it across PHP's nativemail(). Both deliverBccvia a header the MTA strips, set the envelope sender (-f) from the return path orFrom(validated and shell-escaped), and talk through theSendmailProcess/MailFunctionseams for testing.LegacyEmailAdapter, a drop-in replacement for CodeIgniter 4's email service.service('email')now returns the adapter, which exposes the full legacy fluent API (setFrom/setTo/setCC/setBCC/setSubject/setMessage/setAltMessage/setMailType/setHeader/setPriority/setWordWrap/setProtocol/attach/send/printDebugger/batchBCCSend/validation helpers) on top of the newEmailbuilder andMailer. Honors the legacy flatConfig\Emailkeys (protocol,SMTP*,mailPath,mailType,wordWrap/wrapChars,priority,BCCBatch*,DSN) so existing apps need no configuration changes. Invalid addresses are swallowed (send()returnsfalseand never throws). For CI4 parity,Return-PathandReply-Todefault toFrom, and sends flow through the event pipeline (aemail.sendinglistener can cancel). See Legacy Compatibility.- Optional hard word-wrap for the plain-text part: set
Email::$wordWrap(and$wrapChars, default 76) andMessageRendererwraps at word boundaries, leaving long space-less tokens (e.g. URLs) intact. Mailer::fake()and theFakeTransporttest double: swaps the boundservice('mailer')for an in-memory recorder (no network, no real transport) and returns it. Exposes content-matcher assertions —assertSent(),assertSentTo(),assertNotSent(),assertNothingSent(),assertSentCount()— andsent()for inspecting the recorded messages. See Testing Mail.Mailable, an abstract class-based email definition: compose the message inbuild()(which runs lazily at send time) via protectedfrom/to/subject/html/texthelpers, pick a named mailer withtransport(), andsend()to route throughservice('mailer'). Each send tags the message with the Mailable's class. Themake:mailableSpark command scaffolds a class intoapp/Mails/(--forceoverwrites).FakeTransport::sent()andassertSent()now also accept a Mailable class-string (with an optional closure filter) so tests can assert by type. See Mailables.