marko/mail
Mail contracts and message building --- compose emails with a fluent API and send them through any mail driver. This package provides the MailerInterface, the fluent Message builder, Address and Attachment value objects, and an optional ViewMailer decorator for template rendering. It contains no transport implementation; install a driver like marko/mail-smtp or marko/mail-log for actual delivery. Includes a mail:test CLI command for verifying configuration.
This package defines contracts only. Install a driver for implementation:
marko/mail-smtp--- SMTP transportmarko/mail-log--- Log-based transport (development/testing)
Installation
Section titled “Installation”composer require marko/mailNote: You also need an implementation package such as marko/mail-smtp or marko/mail-log.
Building and Sending Messages
Section titled “Building and Sending Messages”Inject the mailer and build messages with the fluent API:
use Marko\Mail\Contracts\MailerInterface;use Marko\Mail\Message;
class WelcomeMailer{ public function __construct( private MailerInterface $mailer, ) {}
public function sendWelcome( string $email, string $name, ): void { $message = Message::create() ->to($email, $name) ->subject('Welcome!') ->html('<h1>Welcome, ' . $name . '!</h1>') ->text('Welcome, ' . $name . '!');
$this->mailer->send($message); }}Attachments
Section titled “Attachments”Attach files from disk or embed inline images:
use Marko\Mail\Message;
$message = Message::create() ->subject('Your Invoice') ->html('<p>Please find your invoice attached.</p>') ->attach('/path/to/invoice.pdf') ->embed('/path/to/logo.png', 'logo-id');Template-Based Emails
Section titled “Template-Based Emails”When marko/view is installed, use the ViewMailer to render templates:
use Marko\Mail\Message;
$message = Message::create() ->subject('Order Confirmation') ->view('emails/order-confirmation') ->with(['order' => $order]);Configuration
Section titled “Configuration”The MailConfig class provides typed access to mail configuration values:
use Marko\Mail\Config\MailConfig;
class MyService{ public function __construct( private MailConfig $mailConfig, ) {}
public function setup(): void { $driver = $this->mailConfig->driver(); $fromAddress = $this->mailConfig->fromAddress(); $fromName = $this->mailConfig->fromName(); $driverConfig = $this->mailConfig->driverConfig('smtp'); }}CLI Commands
Section titled “CLI Commands”| Command | Description |
|---|---|
marko mail:test <email> | Send a test email to verify mail configuration |
marko mail:test <email> --subject="Custom Subject" | Send a test email with a custom subject |
API Reference
Section titled “API Reference”MailerInterface
Section titled “MailerInterface”use Marko\Mail\Contracts\MailerInterface;use Marko\Mail\Message;
public function send(Message $message): bool;public function sendRaw(string $to, string $raw): bool;Both methods throw TransportException on delivery failure.
Message
Section titled “Message”use Marko\Mail\Message;
public static function create(): self;public function to(string $email, ?string $name = null): self;public function cc(string $email, ?string $name = null): self;public function bcc(string $email, ?string $name = null): self;public function from(string $email, ?string $name = null): self;public function replyTo(string $email, ?string $name = null): self;public function subject(string $subject): self;public function html(string $html): self;public function text(string $text): self;public function attach(string $path, ?string $name = null, ?string $mimeType = null): self;public function embed(string $path, string $contentId, ?string $name = null, ?string $mimeType = null): self;public function header(string $name, string $value): self; // throws MessageException if name or value contains CR/LFpublic function priority(int $priority): self;public function view(string $template): self;public function with(array|string $key, mixed $value = null): self;Address
Section titled “Address”use Marko\Mail\Address;
readonly class Address{ public string $email; public ?string $name;
public function toString(): string; public function formatDisplayName(): string;}The Address constructor throws MessageException if the email address is invalid or if the display name contains CR or LF characters (mail header injection protection).
toString() renders Name <email>. formatDisplayName() returns the name unchanged when it is plain RFC 5322 words, and wraps it in a quoted-string (escaping \ and ") when it contains specials such as ,, <, >, ;, @, . or ", so new Address('[email protected]', 'x <[email protected]>, y') renders as "x <[email protected]>, y" <[email protected]>, a single address.
Attachment
Section titled “Attachment”use Marko\Mail\Attachment;
public static function fromPath(string $path, ?string $name = null, ?string $mimeType = null): self;public static function fromContent(string $content, string $name, string $mimeType = 'application/octet-stream'): self;public static function inline(string $path, string $contentId, ?string $name = null, ?string $mimeType = null): self;Every Attachment is validated on creation, so a filename or type taken from user input cannot inject MIME headers. It throws MessageException when:
- the name, mime type or content ID contains a CR, LF or NUL character
- the mime type is not a bare
type/subtypepair of RFC 2045 tokens (parameters such as; charset=UTF-8are rejected) - the content ID uses characters outside the RFC 5322 msg-id set (letters, digits,
.,@and!#$%&'*+-/=?^_`{|}~; no angle brackets or spaces)
The marko/mail-smtp driver renders non-ASCII filenames with RFC 2231 encoding (filename*=UTF-8''...).
MailConfig
Section titled “MailConfig”use Marko\Mail\Config\MailConfig;
public function ensureConfigExists(): void;public function driver(): string;public function fromAddress(): string;public function fromName(): string;public function driverConfig(string $driver): array;Exceptions
Section titled “Exceptions”| Exception | Description |
|---|---|
MailException | Base exception for all mail errors --- includes getContext() and getSuggestion() methods |
MessageException | Thrown for invalid email addresses, missing or invalid attachments, or no recipients |
TransportException | Thrown on delivery failures --- connection errors, TLS failures, authentication failures, unexpected SMTP responses |