Events
Postal fires an event at each step of a send. They're hooks: a place to log delivery, tweak a message just before it goes out, audit failures, or stop a send entirely. If you've used CodeIgniter's event system before, this works exactly the same way.
The four events
Every send walks through these in order:
| Event | When it fires | Listener receives |
|---|---|---|
email.composing |
At the very start, before anything else | Email $message |
email.sending |
Immediately before the transport runs | Email $message |
email.sent |
After a successful delivery | Email $message, SendResult $result |
email.failed |
After the transport reports a failure | Email $message, SendResult $result |
A send fires composing, then sending, then exactly one of sent or failed. If you've set up a suppression list, Postal also fires email.suppressed for each recipient it removes, between composing and sending. The $message passed to your listeners is Postal's working copy — a clone made at the start of the send — so changes you make to the original after calling send() never sneak in.
Listening for an event
Register a listener with CodeIgniter's Events::on(), usually in your app's app/Config/Events.php:
<?php
use CodeIgniter\Events\Events;
use Myth\Postal\Email;
use Myth\Postal\SendResult;
Events::on('email.sent', static function (Email $message, SendResult $result): void {
log_message('info', 'Mail sent to {to}, id {id}', [
'to' => $message->to[0]->email,
'id' => $result->messageId,
]);
});
The sent and failed listeners get the SendResult too, so you can record the provider message ID on success or the error on failure:
<?php
Events::on('email.failed', static function (Email $message, SendResult $result): void {
log_message('error', 'Mail failed: {error}', ['error' => $result->error]);
});
Knowing which Mailable sent it
When a Mailable sends the message, $message->mailableClass holds the Mailable's fully-qualified class name. Messages you build with new Email(), or send through the legacy adapter, carry null. Postal never writes this value into the email's headers, so it stays on your side of the wire:
<?php
Events::on('email.sent', static function (Email $message, SendResult $result): void {
log_message('info', '{mailable} sent, id {id}', [
'mailable' => $message->mailableClass ?? 'ad-hoc email',
'id' => $result->messageId,
]);
});
Modifying the message
composing and sending listeners receive the live message object, so anything you change there goes out with the email. It's the clean place to enforce app-wide rules — add a compliance BCC, stamp a header, or set a default from:
<?php
Events::on('email.composing', static function (Email $message): void {
$message->bcc('archive@example.com');
$message->header('X-App-Version', '2.4.0');
});
Use composing for broad defaults and sending for last-second changes — the message is identical in both, so reach for whichever reads better.
Cancelling a send
Return false from an email.sending listener to abort delivery. The transport is never called, and send() returns a cancelled result:
<?php
Events::on('email.sending', static function (Email $message): bool {
if (str_ends_with($message->to[0]->email, '@blocked.example')) {
return false; // stop the send
}
return true;
});
The caller sees this in the result:
<?php
$result = service('mailer')->send($email);
if ($result->cancelled) {
// a listener stopped it — $result->success is false, nothing was sent
}
Only sending can cancel
Returning false from composing, sent, or failed does nothing — those events are notifications. Cancellation is deliberately limited to sending, the moment right before delivery.
Turning events off
Events fire on every send by default. To silence all of them, including email.suppressed, set $fireEvents to false in your Config\Mailer:
With events off, no listeners run — including any sending listener that would have cancelled — and the message goes straight to the transport. Reach for this when you want a guaranteed-clean send path (a bulk job, say) with zero listener overhead or interference.
Next steps
- Log — see a full message without sending it
- MIME Rendering — what the message looks like on the wire