Zero Framework ships with a lightweight SMTP mailer that follows Laravel-style configuration while keeping the implementation dependency free. Messages are dispatched through the Zero\Lib\Mail\Mailer facade (Mail).
Implementation: Mailer.php, Message.php, Transport/SmtpTransport.php.
use Zero\Lib\Mail\Mailer;
// or via the kernel alias:
use Mail;Build and send a message inside a closure. The closure receives a fresh Message (with the configured default from already applied).
Mail::send(function ($message) {
$message
->to('user@example.test', 'Tofik')
->subject('Welcome!')
->html(view('mail.welcome', ['name' => 'Tofik']));
});Shortcut for one-line emails with no extra headers/attachments.
Mail::raw('ops@example.test', 'Heartbeat', 'still alive');
Mail::raw('user@example.test', 'Hi', '<b>hello</b>', isHtml: true);Get the underlying singleton. Rarely needed — send() and raw() cover the common cases.
$mailer = Mailer::instance();Drop the singleton (mainly for tests when config has changed).
Mailer::reset();Instance form of send(). The static Mailer::send() ultimately calls this.
Mailer::instance()->dispatch(function ($message) {
$message->to($to)->subject($subject)->text($body);
});Both send() and dispatch() validate that the message has a From address (either configured default or set via ->from()) and at least one recipient; otherwise they throw Zero\Lib\Mail\MailException.
The Message instance is what you build inside the send() callback. Every setter returns $this for chaining.
Override the configured default from.
$message->from('no-reply@example.test', 'Zero Framework');$message->replyTo('support@example.test', 'Support Team');Add a recipient. Call multiple times to send to multiple addresses.
$message->to('alice@example.test')->to('bob@example.test', 'Bob');$message->cc('manager@example.test', 'Manager');$message->bcc('audit@example.test');$message->subject('Your invoice is ready');Plain-text body.
$message->text("Hi Tofik,\n\nThanks for signing up.\n");HTML body. Pair with view() for templates.
$message->html(view('mail.welcome', ['user' => $user]));Low-level body setter (use this for non-text/html content types).
$message->body($json, 'application/json');Add a custom header.
$message
->header('X-Mailer', 'Zero/1.0')
->header('X-Priority', '1');Attach in-memory bytes. Build a multipart/mixed message automatically.
$pdf = file_get_contents(storage_path('invoices/INV-42.pdf'));
$message
->subject('Your invoice')
->html(view('mail.invoice', ['user' => $user]))
->attach('INV-42.pdf', $pdf, 'application/pdf');For a file on disk:
$message->attach('report.csv', file_get_contents($path), 'text/csv');| Method | Returns |
|---|---|
->getFrom() |
?array{address, name} |
->getReplyTo() |
?array{address, name} |
->getTo() / ->getCc() / ->getBcc() |
array<int, array{address, name}> |
->getSubject() |
string |
->getBody() |
string |
->getContentType() |
string |
->getCustomHeaders() |
array<string, string> |
->getAttachments() |
array<int, array{filename, content, contentType}> |
->getEnvelopeRecipients() |
unique To + Cc + Bcc list |
->toMimeString() |
full MIME representation (multipart when attachments present) |
$envelope = $message->getEnvelopeRecipients();
$rfc822 = $message->toMimeString();Mail settings live in config/mail.php and are driven by environment variables. Populate the required keys in your .env file:
MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=your-username
MAIL_PASSWORD=secret
MAIL_ENCRYPTION=tls # tls, ssl, or leave blank for none
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME="Zero Framework"
MAIL_TIMEOUT=30
MAIL_HELO_DOMAIN=my-app.local
MAIL_ALLOW_SELF_SIGNED=false
MAIL_VERIFY_PEER=trueKey options:
MAIL_MAILER– only thesmtpdriver is available today.MAIL_HOST/MAIL_PORT– the SMTP endpoint and port (587 for TLS, 465 for SSL).MAIL_ENCRYPTION–tls,ssl, or leave empty for plain connections.MAIL_USERNAME/MAIL_PASSWORD– leave blank for unauthenticated relays.MAIL_FROM_*– default sender information applied to every message.MAIL_TIMEOUT– socket timeout in seconds (default30).MAIL_HELO_DOMAIN– override the domain used in theEHLO/HELOhandshake.MAIL_ALLOW_SELF_SIGNED/MAIL_VERIFY_PEER– toggle TLS certificate verification behaviour.
Use the Mail facade (registered in core/kernel.php) to compose messages. The callback receives a Zero\Lib\Mail\Message instance with fluent helpers for addressing, headers, and content. The default from address is pulled from configuration, but you can override it per message.
use Mail;
Mail::send(function ($mail) {
$mail->to('user@example.com', 'Test User')
->subject('Welcome to Zero Framework')
->html('<p>Thanks for signing up!</p>');
});Mail::send(function ($mail) {
$mail->from('team@example.com', 'Support Team')
->to('user@example.com')
->subject('We updated your account')
->text('Your preferences were updated successfully.');
});Mail::send(function ($mail) {
$mail->to('ops@example.com')
->subject('Queue is backlogged')
->text("Check the workers.\nNothing is being processed.");
});Mail::send(function ($mail) {
$mail->to('customer@example.com')
->cc('support@example.com')
->bcc('auditor@example.com')
->replyTo('noreply@example.com')
->subject('Invoice #2024')
->html(view('emails.invoice', ['invoice' => $invoice]));
});Mail::send(function ($mail) use ($pdfName, $pdfContents) {
$mail->to('customer@example.com')
->subject('Invoice PDF')
->html('<p>Your invoice is attached.</p>')
->attach($pdfName, $pdfContents, 'application/pdf');
});Mail::send(function ($mail) {
$mail->to('partner@example.com')
->subject('Webhook verification')
->header('X-Request-Id', $requestId)
->header('X-Environment', app_env())
->text('Verification payload attached.');
});Each call to to, cc, or bcc appends a recipient, so you can chain or loop.
Mail::send(function ($mail) use ($recipients) {
foreach ($recipients as $recipient) {
$mail->to($recipient['email'], $recipient['name'] ?? null);
}
$mail->subject('Weekly status')
->text('Team, here is the weekly status update.');
});For quick notifications you can skip the callback entirely:
Mail::raw('alerts@example.com', 'Deployment finished', 'All services are green.');Set the fourth argument to true to treat the body as HTML.
The mailer throws Zero\Lib\Mail\MailException when configuration is missing, the server rejects authentication, or a transport error occurs. Wrap calls in a try/catch block if you want to surface user-friendly feedback.
try {
Mail::raw('ops@example.com', 'Heartbeat failed', 'Database is unreachable.');
} catch (Zero\Lib\Mail\MailException $e) {
logger()->error('Failed to send alert email', ['error' => $e->getMessage()]);
}- Only the SMTP driver is implemented; queues and local sendmail integrations are on the roadmap.
- Multipart/alternative payloads are not yet supported.
- TLS verification defaults to secure settings—loosen them only for local development.
Contributions are welcome! See todo.md for potential enhancements.