TL;DR
- You can record outgoing mail — sent, delivered-ish, opened, clicked — using Laravel's own mail events. No SaaS ESP required.
-
MessageSending/MessageSentevents give you the send-side hooks; a tracking pixel handles opens; rewritten links handle clicks. - Gate the whole thing behind a runtime toggle so you can switch it off per environment without a deploy.
- Caveat up front: "opened" via pixel is best-effort, not truth. Know what the signal actually means.
The two send-side events
Laravel fires MessageSending before a message hits the transport and MessageSent after. Listen to both and you get a durable record of every outgoing mail — recipient, subject, a generated message id — without touching your mailables.
class StoreMessageSending
{
public function handle(MessageSending $event): void
{
if (! MailTracking::enabled()) {
return; // runtime toggle — no deploy needed to switch off
}
$event->message->getHeaders()->addTextHeader('X-Mail-Id', (string) Str::uuid());
}
}
Stamping your own id in a header on the way out is what lets you correlate the open and click callbacks back to the exact message later. Register both listeners in the provider and you have a mail history table filling itself.
Opens: a pixel, honestly labelled
An "open" is a 1×1 transparent image at the end of the HTML body pointing at a tracked route. When the mail client loads it, your endpoint marks the message opened and returns the pixel.
Route::get('mail/o/{mailId}', function (string $mailId) {
MailHistory::where('mail_id', $mailId)->update(['opened_at' => now()]);
return response(base64_decode(self::PIXEL), 200)
->header('Content-Type', 'image/png');
})->name('mail.open');
Be honest about what this measures. Many clients block remote images, Apple Mail Privacy Protection pre-fetches them (false positives), and plain-text readers never load it. "Opened" is a soft signal, not a fact. Track it, but don't build billing on it.
Clicks: rewrite links through a redirector
For clicks, rewrite outgoing links to pass through a tracking route that records the hit and then 302s to the real destination.
Route::get('mail/c/{mailId}', function (Request $request, string $mailId) {
MailHistory::where('mail_id', $mailId)->update(['clicked_at' => now()]);
return redirect()->away($request->query('u'));
})->name('mail.click');
One security note: validate or sign the u target. An open redirector is a phishing gift. Sign the URL when you rewrite it and reject anything that fails the signature — don't redirect to arbitrary user-supplied URLs.
Why a runtime toggle matters
Tracking is a policy decision, not a code decision. Some environments (or some recipients, legally) shouldn't be tracked. A runtime flag — a system setting, not a .env read at boot — lets you turn it off instantly and audit the change, without shipping a release.
it('records nothing while tracking is disabled', function () {
MailTracking::disable();
Mail::to('user@example.test')->send(new WelcomeMail());
expect(MailHistory::count())->toBe(0);
});
What the signals actually mean
| Signal | How | Trust level |
|---|---|---|
| Sent |
MessageSending fired |
High — you know you handed it to the transport |
| Delivered | Transport accepted (no bounce) | Medium — accepted ≠ inboxed |
| Opened | Tracking pixel loaded | Low — blocked/pre-fetched by many clients |
| Clicked | Redirector hit | High — a human (or bot) followed the link |
Takeaway
You don't need an external mail service to get visibility into outgoing mail — Laravel's mail events plus a pixel and a signed redirector cover most of it. Just label the signals honestly (opens lie, clicks don't), sign your redirect targets, and put the whole thing behind a runtime toggle so tracking stays a choice, not a hard-coded default.
Top comments (1)
If you rewrite links to be something like
mail/c/34it obfuscates things and lowers trust. You should also do some kind of sanity check - at the very least check its referrer before saving the click to separate on-mail clicks from ones where people have shared the link. You also have no way of knowing who the email's been forwarded to or how many times.You could say that what you're doing is trying to up the signal-to-noise ratio, but anything that relies on spying on people like this is always going to have noise, and as time goes by more and more people are likely to either hide the signal or boost the noise.