Skip to content

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 transport
  • marko/mail-log --- Log-based transport (development/testing)
Terminal window
composer require marko/mail

Note: You also need an implementation package such as marko/mail-smtp or marko/mail-log.

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)
->from('[email protected]', 'My App')
->subject('Welcome!')
->html('<h1>Welcome, ' . $name . '!</h1>')
->text('Welcome, ' . $name . '!');
$this->mailer->send($message);
}
}

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');

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]);

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');
}
}
CommandDescription
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
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.

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/LF
public function priority(int $priority): self;
public function view(string $template): self;
public function with(array|string $key, mixed $value = null): self;
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.

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/subtype pair of RFC 2045 tokens (parameters such as ; charset=UTF-8 are 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''...).

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;
ExceptionDescription
MailExceptionBase exception for all mail errors --- includes getContext() and getSuggestion() methods
MessageExceptionThrown for invalid email addresses, missing or invalid attachments, or no recipients
TransportExceptionThrown on delivery failures --- connection errors, TLS failures, authentication failures, unexpected SMTP responses