# MailTracker: open and click tracking for Laravel mail

> MailTracker is a Laravel package that hooks into outgoing mail, injects a tracking pixel and rewritten links, and stores the rendered message in the database with an admin panel. It is a convenience wrapper, and it trades database size and stored message bodies for visibility.

**jdavidbakr/mail-tracker** — Package for Laravel to inject tracking code into outgoing emails.

- Repository: https://github.com/jdavidbakr/mail-tracker
- Stars: 622 · Forks: 135
- Language: PHP
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/jdavidbakr-mail-tracker

## What MailTracker hooks into inside a Laravel application

MailTracker is a Composer package for Laravel that sits between the framework's mail layer and the message that finally leaves the application. As the README puts it, it hooks into all outgoing emails and injects a tracking code, and it stores the rendered email in the database. There is also an interface to view sent emails, so the package covers three jobs at once: interception, storage and a read-only admin panel over what was sent.

The audience is narrow and obvious. It is for teams running Laravel that send transactional or notification mail and want to answer two questions afterwards: did the recipient open the message, and did they click the links. It is not an SMTP relay, not a marketing campaign manager and not a delivery reputation tool. The events it exposes around bounces and complaints come from the mail provider's own notifications rather than from anything the package measures itself.

The package is a Composer library with the conventional Laravel shape. The repository root holds composer.json, a config directory, a database directory for migrations, a src directory for the code and a tests directory, alongside the usual project files such as phpunit.xml and .travis.yml. That layout matters for upgrades, because a package that publishes configuration and runs migrations is a package that owns a slice of your database schema, and the README treats schema changes as a first-class upgrade concern.

## The tracking mechanism: pixel, rewritten links and stored bodies

Two distinct tracking signals are configured independently, and the config file describes each one precisely.

The `inject-pixel` option, when set to true, injects a tracking pixel into all outgoing HTML emails. That is a remote image reference in the message body, so it only fires for mail clients that load images, and it only applies to HTML mail.

The `track-links` option rewrites all anchor href links to include a tracking link. The described flow is a redirect chain: the rewritten link takes the user back to your own website, the website logs the click, and then it redirects the user to the final destination. Because the tracking link points at your own domain, a click is visible to the package before the recipient reaches the real target.

The third mechanism is storage. By default the content of an email is stored in the `content` column in the database, so the message can be viewed after it has been sent. The README states the alternative plainly: if a lot of emails are sent, storing all content in the database can consume a lot of memory and slow down the database overall, and the content can instead be saved to a file in the file system. The `content-max-size` option overwrites the default maximum length limit for the `content` database field, and the README warns you to update that field's type from `text` if you need more room. The `expire-days` option controls how long an email is retained, and setting it to zero means old emails are never purged from the database.

The `route` and `admin-route` options set the route information for the tracking URLs and for the admin panel, including prefix and middleware, and `admin-template` lets you integrate the panel with an admin panel you already have. `name` sets the app name shown in the admin interface, and `date-format` controls how dates appear there.

## Installing MailTracker with Composer and running its migration

Installation is the standard Laravel package sequence: require the package, publish the config file and the migration, then run the migration. The provider class name has to be given to the publish command so that the correct files are copied.

```bash
composer require jdavidbakr/mail-tracker
php artisan vendor:publish --provider="jdavidbakr\MailTracker\MailTrackerServiceProvider"
php artisan migrate
```

After `vendor:publish`, the config file lands at config/mail-tracker.php and the migration creates the `sent_emails` table along with its related tables. If you intend to store the tracked models on a different database connection, the README says to update the `connection` entry in mail-tracker.php before running the migrations, because the migrations themselves decide where the tables are created.

If your project already manages its own schema for these tables, the package lets you skip its migrations entirely by calling `MailTracker::ignoreMigrations()` from your service provider.

```php
// In AppServiceProvider

public function register()
{
    MailTracker::ignoreMigrations();
}
```

That call has to happen before you migrate, and it means you own the schema going forward. The README does not document a matching way to unpublish the configuration file, so the config file stays in your repository and is expected to be edited by hand over time.

## Opting one message out with the X-No-Track header

Not every message should be tracked, and the package gives you a per-message escape hatch. Adding an `X-No-Track` header to the message prevents tracking for that message. Any random string can be used as the header value, and the README notes that the header is removed from the email before it is sent, so it does not reach the recipient's mail server.

```php
\Mail::send('email.test', [], function ($message) {
    // ... other settings here
    $message->getHeaders()->addTextHeader('X-No-Track',Str::random(10));
});
```

In practice this is the mechanism to reach for when a message contains content you do not want persisted, such as a password reset link, a one-time code, or an invoice with personal data. Because the storage layer keeps a rendered copy of the email in the database, per-message opt-out is the only lever the package offers for excluding content, apart from turning off tracking globally through the config. There is no documented way to keep tracking for a message while storing nothing but the metadata.

## Upgrade paths across major versions and the recipient migration

The README documents the upgrade path from every major version back to 2.0, which is unusual and useful. The 2.x to 3.x step was the breaking one: the events fired around delivery changed, so `PermanentBouncedMessageEvent` now fires only on permanent bounces, complaints raise a new `ComplaintMessageEvent`, and a new `EmailDeliveredEvent` fires for each successful delivery event. The README points to its own documentation for setting up the SES/SNS environment that receives those notifications. If you listen for the old event to catch every undeliverable message, that listener will silently stop covering complaints after the upgrade.

The 3.x to 4.x step carries no breaking changes except that 4.x is for Laravel 7 and later only. The 4.x to 5.x step changed the shape of the recipient data. In 4.x and earlier there was a `recipient` column holding the RFC standard email format, and 5.x stores the name and the email separately. The `recipient` and `sender` columns were removed from the migration, but no migration was added to drop those columns in an existing database, and accessors were added so existing code that reads those model attributes keeps working. Data conversion is handled by a dedicated artisan command, and the columns can be dropped afterwards.

The artisan command that does the conversion is named `mail-tracker:migrate-recipients`.

```bash
mail-tracker:migrate-recipients
```

The command is the one step in an upgrade that cannot be skipped, because the accessors preserve the attribute names while the stored format changes underneath. There is also a note that the migration for the `sent_emails_url_clicked` table changed with version 2.1.13, where the URL column became a `TEXT` field to allow longer URLs, and no migration is included to perform that update on an old system.

## Where MailTracker stops being the right choice

The central limitation is that MailTracker stores message content in your database by default, and the README says so plainly. High-volume transactional systems will accumulate a copy of every rendered message, and the config offers two partial answers: keep the content in the file system instead, and cap it with `content-max-size`. Neither answer removes the storage cost, and the `content` field type has to be widened by hand if you raise the limit. An `expire-days` value of zero, meaning never purge, is available as well, which is the opposite of what a busy system usually wants.

The second limitation is measurement quality. A pixel only reports an open in an HTML mail client that loads remote images, and many clients block remote images by default or ask the user first. Rewritten links measure clicks directly but change every link in the message, including links inside quoted replies and forwarded content, which can corrupt the record. Neither mechanism tells you that a person actually read the message.

The alternative approach worth weighing is provider-side event tracking. The README already points at SES and SNS notifications for delivery, bounce and complaint events, and that route reports on what the mail provider knows without keeping a copy of the message in your database. The difference in approach is direct: MailTracker answers what the recipient did with a specific message, while provider notifications answer what the provider observed about a delivery attempt. Teams whose priority is deliverability and complaint handling, or whose data retention rules forbid storing message bodies, should read the SES and SNS documentation first. Teams that need per-message open and click history are the ones for whom the database cost is worth paying.

On maintenance, the repository is not archived and the last push was on 2026-09-07. The three most recent releases are 8.4 on 2026-09-07, 8.3 on 2026-06-24 and 8.2 on 2026-06-23, so releases are arriving regularly. The package is MIT licensed, which means no runtime fee for the tracking features and no copyleft obligation on an application that uses it.

## Conclusion

MailTracker fits teams that already run Laravel, want a searchable record of sent mail, and accept storing rendered message bodies in their own database. It does not fit teams that cannot keep message content at rest or that need provider-side bounce and complaint reporting. Verify first that your Laravel version is 9.6.0 or higher if you are on Laravel 9, and that your plan for the content column matches your mail volume.

## FAQ

### How do I install MailTracker in a Laravel project?

Require the package with Composer, publish the config file and migration using the MailTrackerServiceProvider, then run the Laravel migration. The config file is written to config/mail-tracker.php.

### How can I stop MailTracker from tracking a specific email?

Add an `X-No-Track` header with any random string to the message. The README states that this prevents tracking and that the header is removed from the email before it is sent.

### Which Laravel version does MailTracker require?

The README states that if you are using Laravel 9.x you must be on version 9.6.0 or higher. Version 4.x and later are for Laravel 7 and up.

### What does the mail-tracker:migrate-recipients command do?

It converts existing `recipient` and `sender` data in the sent_emails table from the old combined RFC format into the separate name and email format introduced in 5.x. After running it you may drop the old columns.

### Can MailTracker store email content outside the database?

Yes. The README describes a configuration option that saves the content to a file in the file system instead of the `content` database column, which is intended for systems that send a lot of mail.

### What events does MailTracker fire for bounces and complaints?

Since version 3.0, `PermanentBouncedMessageEvent` fires only on permanent bounces, complaints raise the new `ComplaintMessageEvent`, and `EmailDeliveredEvent` fires for each successful delivery event. These notifications rely on an SES and SNS environment configured as described in the project documentation.

## Sources

- [Issues](https://github.com/jdavidbakr/mail-tracker/issues)
- [jdavidbakr/mail-tracker on GitHub](https://github.com/jdavidbakr/mail-tracker)
- [License: MIT](https://github.com/jdavidbakr/mail-tracker/blob/master/LICENSE)
- [README](https://github.com/jdavidbakr/mail-tracker/blob/master/README.md)
- [Releases](https://github.com/jdavidbakr/mail-tracker/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jdavidbakr-mail-tracker
