PHPackages                             phattarachai/mail-log-laravel - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. phattarachai/mail-log-laravel

ActiveLibrary[Logging &amp; Monitoring](/categories/logging)

phattarachai/mail-log-laravel
=============================

Self-hosted outbound mail logger for Laravel — captures Mail + Notification sends, groups by Mailable class + model, ships its own Tailwind UI.

v0.2.0(3w ago)011MITPHPPHP ^8.4

Since May 19Pushed 3w agoCompare

[ Source](https://github.com/phattarachai/mail-log-laravel)[ Packagist](https://packagist.org/packages/phattarachai/mail-log-laravel)[ RSS](/packages/phattarachai-mail-log-laravel/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (13)Versions (3)Used By (0)

Mail Log
========

[](#mail-log)

[![Packagist Version](https://camo.githubusercontent.com/d9c23b6960bc7b8df893b9c1a5ab8bb44bfdb4a310689d77cbf3423cfb51c3c3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7068617474617261636861692f6d61696c2d6c6f672d6c61726176656c2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/phattarachai/mail-log-laravel)[![License](https://camo.githubusercontent.com/275f63233a2a51f62294e72dc900788a1899a7aae4d773e83182e70fa1aa3d2a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7068617474617261636861692f6d61696c2d6c6f672d6c61726176656c2e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)

Self-hosted outbound mail logger for Laravel. Captures every `Mail::send(...)` + Notification mail-channel call, groups identical sends together by Mailable class + Eloquent model, and exposes the result at a `/mail-log` dashboard. Ships its own Tailwind UI — no Filament, no Livewire-from-the-package, no `npm install` in the host.

What it gives you
-----------------

[](#what-it-gives-you)

- **One row per template, not per send.** Sending `OrderShippedMail($order)` to ten recipients lands as **one group with `sent_count=10`** + ten event rows. Different `Order` → different group.
- **A `/mail-log` dashboard** styled like Laravel Horizon / Pulse: list of groups (last-sent first), per-group detail page with body preview (sandboxed ``), Sends table with per-recipient outcomes, attachments, "Test send" modal.
- **Per-mailable opt-in via a single trait** — drop `HasMailLog` into a Mailable, return the originating model from `mailLogModel()`, return `$this->withMailLog(new Headers())` from `headers()`. That's the whole integration surface.
- **Failure capture without queue gymnastics.** A `JobFailed` listener locates the most-recent PENDING event for the matching class+model and flips it to FAILED with the exception message.
- **First-class retention.** `MailLogGroup` implements `Prunable`; `php artisan model:prune` (daily via scheduler) cascades old groups + their events.

Install
-------

[](#install)

```
composer require phattarachai/mail-log-laravel
php artisan mail-log:install
```

`mail-log:install` is idempotent. It publishes the package config, the Spatie `laravel-medialibrary` migration (auto-detected when the `media` table is missing), and the package's own migrations. It then writes three env keys (`MAIL_LOG_ENABLED`, `MAIL_LOG_RETENTION_DAYS`, `MAIL_LOG_UI_PATH`) only when absent, and prints the snippets you need to paste into `AppServiceProvider::boot()`, a Mailable, and the scheduler.

Re-run safely. Pass `--dry-run` to print intended changes without writing.

After install, paste the auth gate snippet:

```
use Phattarachai\MailLogLaravel\MailLog;
use Phattarachai\MailLogLaravel\Models\MailLogGroup;

MailLogGroup::registerMorphMap();
MailLog::auth(fn ($request) => $request->user()?->isAdmin() ?? false);
```

The default policy blocks `/mail-log` unless `APP_DEBUG=true` — loud, safe failure until you opt in.

Per-Mailable opt-in
-------------------

[](#per-mailable-opt-in)

Drop the trait into a Mailable and tell it what model originated the send:

```
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Headers;
use Phattarachai\MailLogLaravel\Concerns\HasMailLog;

class OrderShippedMail extends Mailable
{
    use HasMailLog;

    public function __construct(public Order $order) {}

    protected function mailLogModel(): ?\Illuminate\Database\Eloquent\Model
    {
        return $this->order;
    }

    public function headers(): Headers
    {
        return $this->withMailLog(new Headers());
    }
}
```

Now every `Mail::to($recipient)->send(new OrderShippedMail($order))` to ANY recipient lands in the same group; sending the same Mailable for a different `Order` creates a separate group.

### Trait hooks

[](#trait-hooks)

All return safe defaults — override only what you need.

HookReturnsBehavior`mailLogModel(): ?Model``null`The originating record. Folded into the fingerprint when mode includes `'model'` (default).`mailLogNotificationClass(): ?string``null`The source `Notification` when the Mailable is constructed inside `toMail()`. Lets you fingerprint on the notification instead.`mailLogFingerprintHints(): array``[]`Extra strings (tenant id, A/B variant) folded in when mode includes `'hints'`.`mailLogFingerprintMode(): ?array``null`Per-Mailable override of `['class', 'model']`. Valid: `class`, `notification_class`, `model`, `hints`, `subject`, `body`, `mailer`.`mailLogSkip(): bool``false`Opt this Mailable out of capture entirely (health-check pings, transactional one-offs).Group / event model
-------------------

[](#group--event-model)

Two tables in lock-step:

```
mail_log_groups (one row per fingerprint)
├── fingerprint  (CHAR 64, UNIQUE — the dedup key)
├── subject / from / mailable_class / notification_class / mailer  (latest seen)
├── model_type / model_id  (morphTo)
├── html_body / text_body  (first-send wins; representative preview only)
├── sent_count / failed_count / latest_status  (denormalized counters)
└── created_at / updated_at

mail_logs (one row per Mail::send(), append-only)
├── group_id  (FK → mail_log_groups.id, cascadeOnDelete)
├── to / cc / bcc  (JSON)
├── status  (Pending → Sent | Failed)
├── error_message / seconds / sent_at
└── created_at

```

The body shown on the show page is the **first send** in the group — per-recipient body variation (signed URLs, magic-login tokens, personalized greetings) is NOT captured per row. The package favors fingerprint stability over body fidelity in v0.1; per-event body storage is on the v0.2 roadmap behind `MAIL_LOG_STORE_EVENT_BODIES=true`.

Config
------

[](#config)

```
MAIL_LOG_ENABLED=true                    # master switch — false skips listener registration
MAIL_LOG_RETENTION_DAYS=365              # prunable; null = never prune; events cascade with the group
MAIL_LOG_UI_PATH=mail-log                # URL prefix; null skips route registration
MAIL_LOG_TEST_SEND_ENABLED=true          # show the Test-send button + POST /test-send route
MAIL_LOG_MAX_RECIPIENTS_PER_EVENT=200    # cap recipients stored in a single event row's JSON columns
```

See [`config/mail-log.php`](config/mail-log.php) for the full schema; the [Boost skill `reference.md`](resources/boost/skills/mail-log/reference.md) walks every key.

Scheduling retention
--------------------

[](#scheduling-retention)

```
// bootstrap/app.php (Laravel 11+) — inside ->withSchedule(function (Schedule $schedule) { ... }):
$schedule->command('model:prune', [
    '--model' => [\Phattarachai\MailLogLaravel\Models\MailLogGroup::class],
])->daily();
```

Events cascade-delete via FK when their parent group is pruned.

Requirements
------------

[](#requirements)

- PHP `^8.4`
- Laravel `^12` or `^13`
- `spatie/laravel-medialibrary ^11`

Credits
-------

[](#credits)

Built by [Phattarachai Chaimongkol](https://github.com/phattarachai). Same shipping pattern as Laravel [Horizon](https://laravel.com/docs/horizon) and [Pulse](https://laravel.com/docs/pulse) — pre-built CSS + JS committed to `dist/`, inlined into the layout via `MailLog::css()` / `MailLog::js()` helpers, no host-side `npm install` required.

License
-------

[](#license)

MIT. See [LICENSE](LICENSE.md).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance95

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

21d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/62843288?v=4)[phatchai](/maintainers/phatchai)[@phatchai](https://github.com/phatchai)

---

Top Contributors

[![phattarachai](https://avatars.githubusercontent.com/u/2162876?v=4)](https://github.com/phattarachai "phattarachai (6 commits)")

---

Tags

laravelloggingmailobservability

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/phattarachai-mail-log-laravel/health.svg)

```
[![Health](https://phpackages.com/badges/phattarachai-mail-log-laravel/health.svg)](https://phpackages.com/packages/phattarachai-mail-log-laravel)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k14.1M120](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9732.3M121](/packages/roots-acorn)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.4k](/packages/larastan-larastan)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k28.4M134](/packages/laravel-cashier)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

76318.2M110](/packages/laravel-mcp)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
