Skip to content

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:

<?php

public bool $fireEvents = false;

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