PHPackages                             blax-software/laravel-mail - 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. [Mail &amp; Notifications](/categories/mail)
4. /
5. blax-software/laravel-mail

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

blax-software/laravel-mail
==========================

Tracked outbound + IMAP-synced inbound mail for Laravel. Per-mailbox SMTP/IMAP credentials, threaded messages, open/click tracking, CQRS read side, scheduler-friendly poller.

18PHP

Since May 13Pushed 2mo agoCompare

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

READMEChangelogDependenciesVersions (1)Used By (0)

[![Blax Software OSS](https://raw.githubusercontent.com/blax-software/laravel-workkit/master/art/oss-initiative-banner.svg)](https://github.com/blax-software)

Laravel Mail
============

[](#laravel-mail)

[![PHP Version](https://camo.githubusercontent.com/c9f64f714c636ba27a3bba6dfd52f98426832db1262747efa54b212d16943651/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e322d626c7565)](https://php.net)[![Laravel](https://camo.githubusercontent.com/0daef7568c276be3ebf8be36baf2a38ae26298952f1de1c56251d67453e57d48/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c61726176656c2d31302e782d2d31332e782d6f72616e6765)](https://laravel.com)[![License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)](LICENSE)

Per-mailbox SMTP + IMAP, threaded message storage, open / click tracking, CQRS read queries, and a scheduler that polls itself — for Laravel apps that need more than fire-and-forget `Mail::send()`.

Note

Public API may still shift between minor releases. Pin to a tag when you depend on it in production.

Table of contents
-----------------

[](#table-of-contents)

1. [Features](#features)
2. [Requirements](#requirements)
3. [Installation](#installation)
4. [Configuration](#configuration)
5. [Quick start](#quick-start)
6. [Sending mail](#sending-mail)
7. [Receiving mail](#receiving-mail)
8. [Threading](#threading)
9. [Tracking (open / click)](#tracking-open--click)
10. [Events](#events)
11. [CQRS queries](#cqrs-queries)
12. [Models](#models)
13. [Enums](#enums)
14. [Console commands](#console-commands)
15. [Scheduler](#scheduler)
16. [Extending](#extending)
17. [Security](#security)
18. [Credits](#credits)
19. [License](#license)

Features
--------

[](#features)

### Multi-mailbox identity

[](#multi-mailbox-identity)

- **Per-mailbox SMTP + IMAP credentials** stored as Eloquent rows. Each `Mailbox` carries its own host / port / encryption / username / password for both directions. The dispatcher builds a one-shot Laravel mailer per send, so you can ship from `support@`, `billing@`, and `noreply@` from the same Laravel app without touching `config/mail.php`.
- **Encrypted password columns** via Laravel's `encrypted` cast — rotates with `APP_KEY`.
- **Per-row enable flag** + `last_error` + `last_polled_at` so admin UIs can show health without re-reading logs.

### Outbound

[](#outbound)

- **`MailDispatcher::dispatch(OutboundMail)`** — single entry point. Persists a `MailMessage` row (status = `Queued`, with `Message-ID` + tracking token), then queues `SendMailJob` for the real SMTP handshake.
- **3 tries, 60 / 300 second backoff** on transport errors. Terminal failures flip the row to `Failed` and fire `OutboundMailFailed`.
- **Idempotent re-runs** — a queued job whose row is already `Sent` / `Delivered` returns early instead of double-sending.
- **Custom headers, attachments, `Reply-To`, `In-Reply-To`** all first-class on the `OutboundMail` DTO.

### Inbound

[](#inbound)

- **`blax-mail:poll`** command — fetches new messages from each enabled mailbox's IMAP folder, dedupes by `Message-ID`, persists them as inbound `MailMessage` rows with full headers / body / attachments.
- **UID watermarking** (`mailbox.meta.last_imap_uid`) so a mid-batch crash doesn't re-process what already landed.
- **Per-message failure isolation** — a single malformed message logs a warning and the batch moves on. The watermark only advances on successful persists for that UID.
- **Attachment download** to any Laravel `Storage` disk, with size cap.
- **Pure PHP** — uses [`directorytree/imapengine`](https://github.com/DirectoryTree/ImapEngine), no `ext-imap` required.

### Threading

[](#threading)

- **Automatic `In-Reply-To` / `References` matching** against existing outbound `message_id`s — inbound replies attach to their parent without listener wiring.
- **`thread_root_id`** column on every message so a single indexed query returns the whole thread.

### Tracking

[](#tracking)

- **Open pixel** + **click rewrite** added to outbound HTML during dispatch.
- Both endpoints validate a per-message token (TTL-capped via `tracking.token_ttl_days`), record a `MailEvent`, then redirect / serve the pixel.
- Disable globally via `BLAX_MAIL_TRACKING=false`; per-send opt-out is on the roadmap.

### Read side (CQRS)

[](#read-side-cqrs)

- **Three query objects** — `ListMessagesQuery`, `GetThreadQuery`, `FindMessageByMessageIdQuery` — resolved from the container. Composable, mockable, no leaky Eloquent scope chains in your controllers.

### Operational

[](#operational)

- **Auto-scheduled poller** — the package's service provider registers `blax-mail:poll` on the host scheduler (default: every minute, `withoutOverlapping`). No `routes/console.php` boilerplate needed.
- **`blax-mail:cleanup`** purges soft-deleted messages older than `retention.purge_days`.
- **Six events** for routing / observability (see [Events](#events)).
- **Model overrides** via config — swap any of the package's five models for a subclass without forking.

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

[](#requirements)

PHP8.2+Laravel10, 11, 12, or 13Queue driverany (database, redis, sqs, …) — `SendMailJob` implements `ShouldQueue`InboundAn IMAP-accessible mailboxOutboundAn SMTP-accessible mailboxInstallation
------------

[](#installation)

```
composer require blax-software/laravel-mail
php artisan vendor:publish --tag=blax-mail-config
php artisan migrate
```

The service provider is auto-discovered. The poller registers itself on the scheduler automatically — you don't need to add anything to `routes/console.php`.

Configuration
-------------

[](#configuration)

All keys are environment-overridable. Defaults in `config/blax-mail.php`:

```
return [
    'tracking' => [
        'enabled'        => env('BLAX_MAIL_TRACKING', true),
        'route_prefix'   => env('BLAX_MAIL_ROUTE_PREFIX', 'blax-mail/track'),
        'token_ttl_days' => env('BLAX_MAIL_TOKEN_TTL_DAYS', 90),
        'middleware'     => ['web'],
    ],

    'imap' => [
        'default_folder'                 => env('BLAX_MAIL_IMAP_FOLDER', 'INBOX'),
        'fetch_limit'                    => (int) env('BLAX_MAIL_IMAP_FETCH_LIMIT', 200),
        'default_interval_minutes'       => (int) env('BLAX_MAIL_IMAP_INTERVAL', 1),
        'schedule_enabled'               => env('BLAX_MAIL_SCHEDULE_ENABLED', true),
        'poll_cron'                      => env('BLAX_MAIL_POLL_CRON', '* * * * *'),
        'schedule_without_overlapping'   => env('BLAX_MAIL_SCHEDULE_NO_OVERLAP', true),
        'auto_thread'                    => env('BLAX_MAIL_AUTO_THREAD', true),
        'attachments' => [
            'download'    => env('BLAX_MAIL_DOWNLOAD_ATTACHMENTS', true),
            'disk'        => env('BLAX_MAIL_ATTACHMENT_DISK', 'local'),
            'path_prefix' => env('BLAX_MAIL_ATTACHMENT_PATH', 'blax-mail/attachments'),
            'max_bytes'   => (int) env('BLAX_MAIL_ATTACHMENT_MAX_BYTES', 25 * 1024 * 1024),
        ],
    ],

    'outbound' => [
        'default_from_name' => env('BLAX_MAIL_DEFAULT_FROM_NAME', config('app.name')),
        'list_unsubscribe'  => env('BLAX_MAIL_LIST_UNSUBSCRIBE', true),
        'click_tracking'    => env('BLAX_MAIL_CLICK_TRACKING', true),
    ],

    'retention' => [
        'purge_days' => (int) env('BLAX_MAIL_PURGE_DAYS', 365),
    ],

    'models' => [
        'mailbox'         => \Blax\Mail\Models\Mailbox::class,
        'mail_message'    => \Blax\Mail\Models\MailMessage::class,
        'mail_recipient'  => \Blax\Mail\Models\MailRecipient::class,
        'mail_attachment' => \Blax\Mail\Models\MailAttachment::class,
        'mail_event'      => \Blax\Mail\Models\MailEvent::class,
    ],
];
```

Quick start
-----------

[](#quick-start)

### 1. Configure a mailbox

[](#1-configure-a-mailbox)

```
use Blax\Mail\Models\Mailbox;

$box = Mailbox::create([
    'name'            => 'Support',
    'email'           => 'support@example.com',
    'from_name'       => 'ACME Support',

    'smtp_host'       => 'smtp.example.com',
    'smtp_port'       => 587,
    'smtp_encryption' => 'tls',
    'smtp_username'   => 'support@example.com',
    'smtp_password'   => 'secret',     // auto-encrypted on save

    'imap_host'       => 'imap.example.com',
    'imap_port'       => 993,
    'imap_encryption' => 'ssl',
    'imap_username'   => 'support@example.com',
    'imap_password'   => 'secret',     // auto-encrypted on save
    'imap_folder'     => 'INBOX',

    'enabled'         => true,
]);
```

### 2. Send

[](#2-send)

```
use Blax\Mail\Services\MailDispatcher;
use Blax\Mail\DTOs\OutboundMail;

app(MailDispatcher::class)->dispatch(new OutboundMail(
    mailbox:  $box,
    to:       ['tim@example.com'],
    subject:  'Re: Delivery',
    bodyHtml: $html,
    bodyText: $text,
));
```

### 3. Receive

[](#3-receive)

The poller is already scheduled (every minute by default — see [Scheduler](#scheduler)). To process the queue and the scheduler in development:

```
php artisan queue:work       # processes SendMailJob
php artisan schedule:work    # runs blax-mail:poll on its cron
```

Listen for new inbound mail:

```
use Blax\Mail\Events\InboundMailReceived;

Event::listen(InboundMailReceived::class, function (InboundMailReceived $event) {
    // $event->message — the persisted MailMessage row
    // $event->threadParent — the matched outbound parent (null if first contact)
});
```

Sending mail
------------

[](#sending-mail)

### `OutboundMail` DTO

[](#outboundmail-dto)

The full constructor signature:

```
new OutboundMail(
    mailbox:      $box,                    // Blax\Mail\Models\Mailbox — must canSend()
    to:           ['a@example.com'],       // string[]
    subject:      'Hello',                 // string
    bodyHtml:     'Hi',             // string|null
    bodyText:     'Hi',                    // string|null
    cc:           [],                      // string[]
    bcc:          [],                      // string[]
    replyTo:      'support@example.com',   // string|null — overrides mailbox.reply_to for this send
    inReplyTo:    '',  // string|null — stamps In-Reply-To + References headers
    attachments:  [$outboundAttachment],   // OutboundAttachment[]
    headers:      ['X-Campaign-Id' => 'spring-2026'],  // extra mail headers
    subjectType:  'order',                 // string|null — polymorphic hint persisted on the row
    subjectId:    (string) $order->id,     // string|null
    meta:         ['app_mail_id' => 'abc'],// array — free-form, persisted on the row + on every event
);
```

One of `bodyHtml` or `bodyText` is required. The DTO is `final` + readonly — pass it to `MailDispatcher::dispatch()` and that's it.

### What `dispatch()` does

[](#what-dispatch-does)

1. Builds an inbound `MailMessage` row with status `Queued`, a generated `Message-ID`, the canonical body, recipients, attachments, and a tracking token.
2. Logs a `MailEvent` of type `Queued`.
3. Fires `OutboundMailQueued`.
4. Queues `SendMailJob` with the row's id + the DTO.

When the job runs, it:

5. Builds a transient Laravel mailer using the `Mailbox`'s SMTP credentials (mailer name is `blax-mail-` — concurrent sends from different mailboxes don't fight over the same config key).
6. Sends through Symfony Mailer, stamps the canonical `Message-ID`, injects the tracking pixel + link rewrites.
7. On success: row → `Sent`, fires `OutboundMailSent`.
8. On all retries exhausted: row → `Failed`, fires `OutboundMailFailed`.

Receiving mail
--------------

[](#receiving-mail)

The poller (`Blax\Mail\Services\ImapPoller`) iterates every enabled mailbox, fetches messages above the watermark, persists them as inbound `MailMessage` rows, and fires `InboundMailReceived` for each.

### Watermarking

[](#watermarking)

`mailbox.meta.last_imap_uid` advances only when a UID processes cleanly. A mid-batch failure leaves the watermark where it was, so the next poll retries the same UIDs — no message loss on transient errors.

### What lands on the row

[](#what-lands-on-the-row)

ColumnSource`message_id`RFC 5322 `Message-ID` header, normalized to `` (synthesized when missing)`in_reply_to`First `In-Reply-To` value (multi-value headers collapsed)`references`Raw `References` value`subject`, `body_text`, `body_html`, `raw_headers`Parsed from the IMAP message`from_address`, `from_name`Decoded address header`to`, `cc`, `bcc`Address lists (also persisted to `MailRecipient` rows for indexed lookups)`received_at`IMAP date header, falls back to `now()``meta.imap_uid`The fetched UID for diagnosticsAttachments are persisted to `MailAttachment` rows. When `imap.attachments.download = true` the bytes are streamed to the configured disk; oversized attachments (&gt; `max_bytes`) skip the download but keep the metadata.

Threading
---------

[](#threading-1)

When `imap.auto_thread = true` (default), the poller's `MessageThreader` matches each inbound's `In-Reply-To` / `References` against existing outbound `message_id`s. On a hit:

- The inbound row's `thread_root_id` points at the outbound parent.
- The `parent_id` column points at the immediate ancestor in the thread.
- `MailEvent::Threaded` records the match.

To walk the whole thread:

```
use Blax\Mail\Queries\GetThreadQuery;

$thread = app(GetThreadQuery::class)
    ->forMessage($message)
    ->execute();   // Collection, ordered by created_at
```

Apps that prefer their own threading set `BLAX_MAIL_AUTO_THREAD=false` and subscribe to `InboundMailReceived`.

Tracking (open / click)
-----------------------

[](#tracking-open--click)

`config('blax-mail.tracking.enabled')` controls the full open/click pipeline. When enabled, outbound HTML is rewritten at dispatch time:

- A 1×1 transparent GIF `` pointing at `/{route_prefix}/open/{token}.gif` is appended.
- Every `` is rewritten to `/{route_prefix}/click/{token}?u={signed-target}`.

Both endpoints validate the token, record a `MailEvent` (`Opened` / `Clicked`), then redirect / serve the pixel. Past `token_ttl_days` the pixel still returns 200 OK (mail clients don't mark the message broken) but no event is logged.

Route nameURIBehaviour`blax-mail.tracking.open``GET {prefix}/open/{token}.gif`1×1 GIF, fires `MailOpened``blax-mail.tracking.click``GET {prefix}/click/{token}?u={target}`302 to `target`, records `MailEvent::Clicked`The text body is never rewritten — plain-text alternatives stay untouched.

Events
------

[](#events)

EventPayloadWhen`OutboundMailQueued``MailMessage``dispatch()` persisted the row + queued the send`OutboundMailSent``MailMessage`SMTP accepted the message`OutboundMailFailed``MailMessage`, `Throwable`All retries exhausted`InboundMailReceived``MailMessage`, `?MailMessage $threadParent`Poller persisted an inbound row`MailOpened``MailMessage`, `?string $userAgent`, `?string $ip`Tracking pixel hit> Click events are currently persisted as `MailEvent::Clicked` rows but no dedicated `MailClicked` event class is fired yet — subscribe to the underlying model events if you need the hook today.

CQRS queries
------------

[](#cqrs-queries)

Three query objects in `Blax\Mail\Queries`. Resolve from the container, chain builders, call `execute()`:

### `ListMessagesQuery`

[](#listmessagesquery)

```
$inbox = app(ListMessagesQuery::class)
    ->forMailbox($box->id)
    ->inboundOnly()
    ->unread()
    ->since(now()->subWeek())
    ->limit(50)
    ->execute();          // Collection

// or paginate:
$page = app(ListMessagesQuery::class)
    ->forMailbox($box->id)
    ->outboundOnly()
    ->withStatus(MailStatus::Sent)
    ->paginate(25);
```

Builders: `forMailbox()`, `direction()`, `inboundOnly()`, `outboundOnly()`, `withStatus()`, `unread()`, `forSubject($type, $id)`, `since()`, `until()`, `limit()`, `execute()`, `paginate()`.

### `GetThreadQuery`

[](#getthreadquery)

```
$thread = app(GetThreadQuery::class)
    ->forMessage($message)
    ->execute();          // Collection, ordered chronologically
```

### `FindMessageByMessageIdQuery`

[](#findmessagebymessageidquery)

```
$msg = app(FindMessageByMessageIdQuery::class)
    ->execute('');   // ?MailMessage
```

Models
------

[](#models)

ClassTablePurpose`Blax\Mail\Models\Mailbox``mailboxes`Per-identity SMTP + IMAP config + watermark`Blax\Mail\Models\MailMessage``mail_messages`One row per sent / received message`Blax\Mail\Models\MailRecipient``mail_recipients`Normalized address-per-row for indexed `forSubject` lookups`Blax\Mail\Models\MailAttachment``mail_attachments`Filename, mime, size, storage path`Blax\Mail\Models\MailEvent``mail_events`Audit log: `Queued` / `Sent` / `Opened` / `Clicked` / …`MailMessage` also exposes:

```
$message->mailbox;      // BelongsTo Mailbox
$message->recipients;   // HasMany MailRecipient
$message->attachments;  // HasMany MailAttachment
$message->events;       // HasMany MailEvent (audit timeline)
$message->subject;      // MorphTo — resolves the polymorphic subject if set
$message->thread();     // Whole thread as a Collection
$message->parent();     // Immediate parent in the thread, or null
$message->isInbound();  // bool
$message->isOutbound(); // bool
$message->markRead();   // status → Read
```

### Swapping a model

[](#swapping-a-model)

```
// config/blax-mail.php
'models' => [
    'mail_message' => \App\Models\MyMailMessage::class,   // extends Blax\Mail\Models\MailMessage
],
```

The package resolves every model via `config('blax-mail.models.X')`, so a subclass slots in without touching the core code.

Enums
-----

[](#enums)

`Blax\Mail\Enums\MailDirection`:

CaseValue`Outbound``outbound``Inbound``inbound``Blax\Mail\Enums\MailStatus`:

CaseValueNotes`Queued``queued`Outbound — persisted, awaiting SMTP`Sending``sending`Outbound — `SendMailJob` is running`Sent``sent`Outbound — SMTP accepted`Delivered``delivered`Outbound — confirmed delivered (provider-dependent)`Bounced``bounced`Outbound — provider reported bounce`Failed``failed`Outbound — all retries exhausted`Received``received`Inbound — fresh from poller`Read``read`Inbound — `markRead()` was called`Blax\Mail\Enums\MailEventType` (audit log entries): `Queued`, `Sent`, `Delivered`, `Bounced`, `Complaint`, `Failed`, `Opened`, `Clicked`, `Received`, `Threaded`.

Console commands
----------------

[](#console-commands)

```
# Poll every enabled mailbox once (typically invoked by the scheduler).
php artisan blax-mail:poll

# Restrict to one mailbox (matches by id, email, or name):
php artisan blax-mail:poll --mailbox=support@example.com

# Hard-delete soft-deleted messages older than retention.purge_days.
php artisan blax-mail:cleanup
```

Scheduler
---------

[](#scheduler)

The package self-registers `blax-mail:poll` on the host scheduler via `callAfterResolving(Schedule::class, …)` — the binding only resolves inside `schedule:run` / `schedule:work`, so web requests and other commands pay nothing.

Defaults:

```
cron expression       * * * * *      every minute
without overlapping   true           mutex prevents queue-up

```

Override per environment:

```
BLAX_MAIL_POLL_CRON="*/5 * * * *"          # poll every 5 minutes
BLAX_MAIL_SCHEDULE_NO_OVERLAP=false        # allow parallel polls
BLAX_MAIL_SCHEDULE_ENABLED=false           # disable auto-schedule, register manually
```

If you set `BLAX_MAIL_SCHEDULE_ENABLED=false`, register the command yourself:

```
// routes/console.php
Schedule::command('blax-mail:poll')->everyTenMinutes()->withoutOverlapping();
```

Extending
---------

[](#extending)

### React to inbound mail

[](#react-to-inbound-mail)

```
use Blax\Mail\Events\InboundMailReceived;

Event::listen(InboundMailReceived::class, function (InboundMailReceived $event) {
    // $event->message       — Blax\Mail\Models\MailMessage (already persisted)
    // $event->threadParent  — ?MailMessage (the matched outbound, or null)

    // Typical pattern: file the inbound onto your own domain pivot.
    // Walk threadParent → meta.app_mail_id → your domain Mail row, then clone
    // its M:N subject linkage onto the inbound so it surfaces on every
    // entity feed the original outbound was filed under.
});
```

### Custom threading

[](#custom-threading)

```
BLAX_MAIL_AUTO_THREAD=false
```

Then subscribe to `InboundMailReceived` and set `thread_root_id` / `parent_id` yourself.

### Custom transport / storage

[](#custom-transport--storage)

Bind your own implementation:

```
// AppServiceProvider::register
$this->app->singleton(\Blax\Mail\Contracts\Dispatcher::class, MyDispatcher::class);
$this->app->singleton(\Blax\Mail\Contracts\Poller::class, MyPoller::class);
```

Both contracts have one method (`dispatch(OutboundMail): MailMessage`, `poll(Mailbox): int`).

Security
--------

[](#security)

Please report vulnerabilities by email: ****. We'll acknowledge within 72 hours.

Credits
-------

[](#credits)

- [Fabian Wagner](https://github.com/fabianwagner)
- [All Contributors](../../contributors)

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

Star History
------------

[](#star-history)

[    ![Star History Chart](https://camo.githubusercontent.com/63192d53ea4d751deb9bedd7eebf6cc852c9bf3781919c54602006bbf1d4e2a6/68747470733a2f2f6170692e737461722d686973746f72792e636f6d2f63686172743f7265706f733d626c61782d736f6674776172652f6c61726176656c2d6d61696c26747970653d64617465266c6567656e643d746f702d6c656674) ](https://www.star-history.com/?repos=blax-software%2Flaravel-mail&type=date&legend=top-left)

###  Health Score

20

—

LowBetter than 12% of packages

Maintenance57

Moderate activity, may be stable

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 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.

### Community

Maintainers

![](https://www.gravatar.com/avatar/2d548acfc3520e2f810e35cfb78230f885befda9fa26a3be42353723c48f991e?d=identicon)[blax-software](/maintainers/blax-software)

---

Top Contributors

[![justsomexanda](https://avatars.githubusercontent.com/u/48434066?v=4)](https://github.com/justsomexanda "justsomexanda (5 commits)")

### Embed Badge

![Health badge](/badges/blax-software-laravel-mail/health.svg)

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

PHPackages © 2026

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