PHPackages                             vsevolod-vl/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. vsevolod-vl/mail-log-laravel

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

vsevolod-vl/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.3.1(yesterday)01↑2900%MITPHPPHP &gt;=8.3

Since Aug 27Pushed yesterdayCompare

[ Source](https://github.com/Vsevolod-VL/mail-log-laravel)[ Packagist](https://packagist.org/packages/vsevolod-vl/mail-log-laravel)[ RSS](/packages/vsevolod-vl-mail-log-laravel/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (17)Versions (3)Used By (0)

Mail Log
========

[](#mail-log)

[![Packagist Version](https://camo.githubusercontent.com/a39c2b95a5f7cf0c260e003f706e9572d97cad8c05f04eadc5bba9609cf4f922/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f567365766f6c6f642d564c2f6d61696c2d6c6f672d6c61726176656c2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/Vsevolod-VL/mail-log-laravel)[![License](https://camo.githubusercontent.com/7f1dbf935a327f3d434c150c48ec196ab28d41bc4ff9fe284117cae17723595d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f567365766f6c6f642d564c2f6d61696c2d6c6f672d6c61726176656c2e7376673f7374796c653d666c61742d737175617265)](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 Vsevolod-VL/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 Vsevolod-VL\MailLogLaravel\MailLog;
use Vsevolod-VL\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 Vsevolod-VL\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' => [\Vsevolod-VL\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 [Vsevolod-VL Chaimongkol](https://github.com/Vsevolod-VL). 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

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 75% 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

1d ago

PHP version history (2 changes)v0.3.0PHP ^8.4

v0.3.1PHP &gt;=8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/981b35b8624ad629e7ec86798e97054db9e99c635cb48c70c435cdbae8de5059?d=identicon)[VsevolodVL](/maintainers/VsevolodVL)

---

Top Contributors

[![phattarachai](https://avatars.githubusercontent.com/u/2162876?v=4)](https://github.com/phattarachai "phattarachai (9 commits)")[![Vsevolod-VL](https://avatars.githubusercontent.com/u/207826570?v=4)](https://github.com/Vsevolod-VL "Vsevolod-VL (3 commits)")

---

Tags

laravelloggingmailobservability

###  Code Quality

TestsPest

Static AnalysisRector

Code StyleLaravel Pint

### Embed Badge

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

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

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[laravel/cashier

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

2.5k31.8M163](/packages/laravel-cashier)[laravel/pulse

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

1.7k16.3M160](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9922.4M148](/packages/roots-acorn)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

80427.1M252](/packages/laravel-mcp)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)

PHPackages © 2026

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