{"id":50377009,"url":"https://github.com/blax-software/laravel-mail","last_synced_at":"2026-05-30T10:02:21.172Z","repository":{"id":357540539,"uuid":"1237406725","full_name":"blax-software/laravel-mail","owner":"blax-software","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-13T08:12:51.000Z","size":53,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-13T08:36:14.047Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/blax-software.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-13T06:44:39.000Z","updated_at":"2026-05-13T08:12:57.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/blax-software/laravel-mail","commit_stats":null,"previous_names":["blax-software/laravel-mail"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/blax-software/laravel-mail","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blax-software%2Flaravel-mail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blax-software%2Flaravel-mail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blax-software%2Flaravel-mail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blax-software%2Flaravel-mail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/blax-software","download_url":"https://codeload.github.com/blax-software/laravel-mail/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blax-software%2Flaravel-mail/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33687722,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-30T10:02:20.203Z","updated_at":"2026-05-30T10:02:21.166Z","avatar_url":"https://github.com/blax-software.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Blax Software OSS](https://raw.githubusercontent.com/blax-software/laravel-workkit/master/art/oss-initiative-banner.svg)](https://github.com/blax-software)\n\n# Laravel Mail\n\n[![PHP Version](https://img.shields.io/badge/php-%5E8.2-blue)](https://php.net)\n[![Laravel](https://img.shields.io/badge/laravel-10.x--13.x-orange)](https://laravel.com)\n[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n\nPer-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()`.\n\n\u003e [!NOTE]\n\u003e Public API may still shift between minor releases. Pin to a tag when you depend on it in production.\n\n## Table of contents\n\n1. [Features](#features)\n2. [Requirements](#requirements)\n3. [Installation](#installation)\n4. [Configuration](#configuration)\n5. [Quick start](#quick-start)\n6. [Sending mail](#sending-mail)\n7. [Receiving mail](#receiving-mail)\n8. [Threading](#threading)\n9. [Tracking (open / click)](#tracking-open--click)\n10. [Events](#events)\n11. [CQRS queries](#cqrs-queries)\n12. [Models](#models)\n13. [Enums](#enums)\n14. [Console commands](#console-commands)\n15. [Scheduler](#scheduler)\n16. [Extending](#extending)\n17. [Security](#security)\n18. [Credits](#credits)\n19. [License](#license)\n\n## Features\n\n### Multi-mailbox identity\n\n- **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`.\n- **Encrypted password columns** via Laravel's `encrypted` cast — rotates with `APP_KEY`.\n- **Per-row enable flag** + `last_error` + `last_polled_at` so admin UIs can show health without re-reading logs.\n\n### Outbound\n\n- **`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.\n- **3 tries, 60 / 300 second backoff** on transport errors. Terminal failures flip the row to `Failed` and fire `OutboundMailFailed`.\n- **Idempotent re-runs** — a queued job whose row is already `Sent` / `Delivered` returns early instead of double-sending.\n- **Custom headers, attachments, `Reply-To`, `In-Reply-To`** all first-class on the `OutboundMail` DTO.\n\n### Inbound\n\n- **`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.\n- **UID watermarking** (`mailbox.meta.last_imap_uid`) so a mid-batch crash doesn't re-process what already landed.\n- **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.\n- **Attachment download** to any Laravel `Storage` disk, with size cap.\n- **Pure PHP** — uses [`directorytree/imapengine`](https://github.com/DirectoryTree/ImapEngine), no `ext-imap` required.\n\n### Threading\n\n- **Automatic `In-Reply-To` / `References` matching** against existing outbound `message_id`s — inbound replies attach to their parent without listener wiring.\n- **`thread_root_id`** column on every message so a single indexed query returns the whole thread.\n\n### Tracking\n\n- **Open pixel** + **click rewrite** added to outbound HTML during dispatch.\n- Both endpoints validate a per-message token (TTL-capped via `tracking.token_ttl_days`), record a `MailEvent`, then redirect / serve the pixel.\n- Disable globally via `BLAX_MAIL_TRACKING=false`; per-send opt-out is on the roadmap.\n\n### Read side (CQRS)\n\n- **Three query objects** — `ListMessagesQuery`, `GetThreadQuery`, `FindMessageByMessageIdQuery` — resolved from the container. Composable, mockable, no leaky Eloquent scope chains in your controllers.\n\n### Operational\n\n- **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.\n- **`blax-mail:cleanup`** purges soft-deleted messages older than `retention.purge_days`.\n- **Six events** for routing / observability (see [Events](#events)).\n- **Model overrides** via config — swap any of the package's five models for a subclass without forking.\n\n## Requirements\n\n| | |\n|---|---|\n| PHP | 8.2+ |\n| Laravel | 10, 11, 12, or 13 |\n| Queue driver | any (database, redis, sqs, …) — `SendMailJob` implements `ShouldQueue` |\n| Inbound | An IMAP-accessible mailbox |\n| Outbound | An SMTP-accessible mailbox |\n\n## Installation\n\n```bash\ncomposer require blax-software/laravel-mail\nphp artisan vendor:publish --tag=blax-mail-config\nphp artisan migrate\n```\n\nThe service provider is auto-discovered. The poller registers itself on the scheduler automatically — you don't need to add anything to `routes/console.php`.\n\n## Configuration\n\nAll keys are environment-overridable. Defaults in `config/blax-mail.php`:\n\n```php\nreturn [\n    'tracking' =\u003e [\n        'enabled'        =\u003e env('BLAX_MAIL_TRACKING', true),\n        'route_prefix'   =\u003e env('BLAX_MAIL_ROUTE_PREFIX', 'blax-mail/track'),\n        'token_ttl_days' =\u003e env('BLAX_MAIL_TOKEN_TTL_DAYS', 90),\n        'middleware'     =\u003e ['web'],\n    ],\n\n    'imap' =\u003e [\n        'default_folder'                 =\u003e env('BLAX_MAIL_IMAP_FOLDER', 'INBOX'),\n        'fetch_limit'                    =\u003e (int) env('BLAX_MAIL_IMAP_FETCH_LIMIT', 200),\n        'default_interval_minutes'       =\u003e (int) env('BLAX_MAIL_IMAP_INTERVAL', 1),\n        'schedule_enabled'               =\u003e env('BLAX_MAIL_SCHEDULE_ENABLED', true),\n        'poll_cron'                      =\u003e env('BLAX_MAIL_POLL_CRON', '* * * * *'),\n        'schedule_without_overlapping'   =\u003e env('BLAX_MAIL_SCHEDULE_NO_OVERLAP', true),\n        'auto_thread'                    =\u003e env('BLAX_MAIL_AUTO_THREAD', true),\n        'attachments' =\u003e [\n            'download'    =\u003e env('BLAX_MAIL_DOWNLOAD_ATTACHMENTS', true),\n            'disk'        =\u003e env('BLAX_MAIL_ATTACHMENT_DISK', 'local'),\n            'path_prefix' =\u003e env('BLAX_MAIL_ATTACHMENT_PATH', 'blax-mail/attachments'),\n            'max_bytes'   =\u003e (int) env('BLAX_MAIL_ATTACHMENT_MAX_BYTES', 25 * 1024 * 1024),\n        ],\n    ],\n\n    'outbound' =\u003e [\n        'default_from_name' =\u003e env('BLAX_MAIL_DEFAULT_FROM_NAME', config('app.name')),\n        'list_unsubscribe'  =\u003e env('BLAX_MAIL_LIST_UNSUBSCRIBE', true),\n        'click_tracking'    =\u003e env('BLAX_MAIL_CLICK_TRACKING', true),\n    ],\n\n    'retention' =\u003e [\n        'purge_days' =\u003e (int) env('BLAX_MAIL_PURGE_DAYS', 365),\n    ],\n\n    'models' =\u003e [\n        'mailbox'         =\u003e \\Blax\\Mail\\Models\\Mailbox::class,\n        'mail_message'    =\u003e \\Blax\\Mail\\Models\\MailMessage::class,\n        'mail_recipient'  =\u003e \\Blax\\Mail\\Models\\MailRecipient::class,\n        'mail_attachment' =\u003e \\Blax\\Mail\\Models\\MailAttachment::class,\n        'mail_event'      =\u003e \\Blax\\Mail\\Models\\MailEvent::class,\n    ],\n];\n```\n\n## Quick start\n\n### 1. Configure a mailbox\n\n```php\nuse Blax\\Mail\\Models\\Mailbox;\n\n$box = Mailbox::create([\n    'name'            =\u003e 'Support',\n    'email'           =\u003e 'support@example.com',\n    'from_name'       =\u003e 'ACME Support',\n\n    'smtp_host'       =\u003e 'smtp.example.com',\n    'smtp_port'       =\u003e 587,\n    'smtp_encryption' =\u003e 'tls',\n    'smtp_username'   =\u003e 'support@example.com',\n    'smtp_password'   =\u003e 'secret',     // auto-encrypted on save\n\n    'imap_host'       =\u003e 'imap.example.com',\n    'imap_port'       =\u003e 993,\n    'imap_encryption' =\u003e 'ssl',\n    'imap_username'   =\u003e 'support@example.com',\n    'imap_password'   =\u003e 'secret',     // auto-encrypted on save\n    'imap_folder'     =\u003e 'INBOX',\n\n    'enabled'         =\u003e true,\n]);\n```\n\n### 2. Send\n\n```php\nuse Blax\\Mail\\Services\\MailDispatcher;\nuse Blax\\Mail\\DTOs\\OutboundMail;\n\napp(MailDispatcher::class)-\u003edispatch(new OutboundMail(\n    mailbox:  $box,\n    to:       ['tim@example.com'],\n    subject:  'Re: Delivery',\n    bodyHtml: $html,\n    bodyText: $text,\n));\n```\n\n### 3. Receive\n\nThe poller is already scheduled (every minute by default — see [Scheduler](#scheduler)). To process the queue and the scheduler in development:\n\n```bash\nphp artisan queue:work       # processes SendMailJob\nphp artisan schedule:work    # runs blax-mail:poll on its cron\n```\n\nListen for new inbound mail:\n\n```php\nuse Blax\\Mail\\Events\\InboundMailReceived;\n\nEvent::listen(InboundMailReceived::class, function (InboundMailReceived $event) {\n    // $event-\u003emessage — the persisted MailMessage row\n    // $event-\u003ethreadParent — the matched outbound parent (null if first contact)\n});\n```\n\n## Sending mail\n\n### `OutboundMail` DTO\n\nThe full constructor signature:\n\n```php\nnew OutboundMail(\n    mailbox:      $box,                    // Blax\\Mail\\Models\\Mailbox — must canSend()\n    to:           ['a@example.com'],       // string[]\n    subject:      'Hello',                 // string\n    bodyHtml:     '\u003cp\u003eHi\u003c/p\u003e',             // string|null\n    bodyText:     'Hi',                    // string|null\n    cc:           [],                      // string[]\n    bcc:          [],                      // string[]\n    replyTo:      'support@example.com',   // string|null — overrides mailbox.reply_to for this send\n    inReplyTo:    '\u003cmsg-id@example.com\u003e',  // string|null — stamps In-Reply-To + References headers\n    attachments:  [$outboundAttachment],   // OutboundAttachment[]\n    headers:      ['X-Campaign-Id' =\u003e 'spring-2026'],  // extra mail headers\n    subjectType:  'order',                 // string|null — polymorphic hint persisted on the row\n    subjectId:    (string) $order-\u003eid,     // string|null\n    meta:         ['app_mail_id' =\u003e 'abc'],// array — free-form, persisted on the row + on every event\n);\n```\n\nOne of `bodyHtml` or `bodyText` is required. The DTO is `final` + readonly — pass it to `MailDispatcher::dispatch()` and that's it.\n\n### What `dispatch()` does\n\n1. Builds an inbound `MailMessage` row with status `Queued`, a generated `Message-ID`, the canonical body, recipients, attachments, and a tracking token.\n2. Logs a `MailEvent` of type `Queued`.\n3. Fires `OutboundMailQueued`.\n4. Queues `SendMailJob` with the row's id + the DTO.\n\nWhen the job runs, it:\n\n5. Builds a transient Laravel mailer using the `Mailbox`'s SMTP credentials (mailer name is `blax-mail-\u003cmailbox-id\u003e` — concurrent sends from different mailboxes don't fight over the same config key).\n6. Sends through Symfony Mailer, stamps the canonical `Message-ID`, injects the tracking pixel + link rewrites.\n7. On success: row → `Sent`, fires `OutboundMailSent`.\n8. On all retries exhausted: row → `Failed`, fires `OutboundMailFailed`.\n\n## Receiving mail\n\nThe 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.\n\n### Watermarking\n\n`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.\n\n### What lands on the row\n\n| Column | Source |\n|---|---|\n| `message_id` | RFC 5322 `Message-ID` header, normalized to `\u003cid@host\u003e` (synthesized when missing) |\n| `in_reply_to` | First `In-Reply-To` value (multi-value headers collapsed) |\n| `references` | Raw `References` value |\n| `subject`, `body_text`, `body_html`, `raw_headers` | Parsed from the IMAP message |\n| `from_address`, `from_name` | Decoded address header |\n| `to`, `cc`, `bcc` | Address lists (also persisted to `MailRecipient` rows for indexed lookups) |\n| `received_at` | IMAP date header, falls back to `now()` |\n| `meta.imap_uid` | The fetched UID for diagnostics |\n\nAttachments are persisted to `MailAttachment` rows. When `imap.attachments.download = true` the bytes are streamed to the configured disk; oversized attachments (\u003e `max_bytes`) skip the download but keep the metadata.\n\n## Threading\n\nWhen `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:\n\n- The inbound row's `thread_root_id` points at the outbound parent.\n- The `parent_id` column points at the immediate ancestor in the thread.\n- `MailEvent::Threaded` records the match.\n\nTo walk the whole thread:\n\n```php\nuse Blax\\Mail\\Queries\\GetThreadQuery;\n\n$thread = app(GetThreadQuery::class)\n    -\u003eforMessage($message)\n    -\u003eexecute();   // Collection\u003cMailMessage\u003e, ordered by created_at\n```\n\nApps that prefer their own threading set `BLAX_MAIL_AUTO_THREAD=false` and subscribe to `InboundMailReceived`.\n\n## Tracking (open / click)\n\n`config('blax-mail.tracking.enabled')` controls the full open/click pipeline. When enabled, outbound HTML is rewritten at dispatch time:\n\n- A 1×1 transparent GIF `\u003cimg\u003e` pointing at `/{route_prefix}/open/{token}.gif` is appended.\n- Every `\u003ca href\u003e` is rewritten to `/{route_prefix}/click/{token}?u={signed-target}`.\n\nBoth 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.\n\n| Route name | URI | Behaviour |\n|---|---|---|\n| `blax-mail.tracking.open` | `GET {prefix}/open/{token}.gif` | 1×1 GIF, fires `MailOpened` |\n| `blax-mail.tracking.click` | `GET {prefix}/click/{token}?u={target}` | 302 to `target`, records `MailEvent::Clicked` |\n\nThe text body is never rewritten — plain-text alternatives stay untouched.\n\n## Events\n\n| Event | Payload | When |\n|---|---|---|\n| `OutboundMailQueued` | `MailMessage` | `dispatch()` persisted the row + queued the send |\n| `OutboundMailSent` | `MailMessage` | SMTP accepted the message |\n| `OutboundMailFailed` | `MailMessage`, `Throwable` | All retries exhausted |\n| `InboundMailReceived` | `MailMessage`, `?MailMessage $threadParent` | Poller persisted an inbound row |\n| `MailOpened` | `MailMessage`, `?string $userAgent`, `?string $ip` | Tracking pixel hit |\n\n\u003e 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.\n\n## CQRS queries\n\nThree query objects in `Blax\\Mail\\Queries`. Resolve from the container, chain builders, call `execute()`:\n\n### `ListMessagesQuery`\n\n```php\n$inbox = app(ListMessagesQuery::class)\n    -\u003eforMailbox($box-\u003eid)\n    -\u003einboundOnly()\n    -\u003eunread()\n    -\u003esince(now()-\u003esubWeek())\n    -\u003elimit(50)\n    -\u003eexecute();          // Collection\u003cMailMessage\u003e\n\n// or paginate:\n$page = app(ListMessagesQuery::class)\n    -\u003eforMailbox($box-\u003eid)\n    -\u003eoutboundOnly()\n    -\u003ewithStatus(MailStatus::Sent)\n    -\u003epaginate(25);\n```\n\nBuilders: `forMailbox()`, `direction()`, `inboundOnly()`, `outboundOnly()`, `withStatus()`, `unread()`, `forSubject($type, $id)`, `since()`, `until()`, `limit()`, `execute()`, `paginate()`.\n\n### `GetThreadQuery`\n\n```php\n$thread = app(GetThreadQuery::class)\n    -\u003eforMessage($message)\n    -\u003eexecute();          // Collection\u003cMailMessage\u003e, ordered chronologically\n```\n\n### `FindMessageByMessageIdQuery`\n\n```php\n$msg = app(FindMessageByMessageIdQuery::class)\n    -\u003eexecute('\u003cmsg-id@example.com\u003e');   // ?MailMessage\n```\n\n## Models\n\n| Class | Table | Purpose |\n|---|---|---|\n| `Blax\\Mail\\Models\\Mailbox` | `mailboxes` | Per-identity SMTP + IMAP config + watermark |\n| `Blax\\Mail\\Models\\MailMessage` | `mail_messages` | One row per sent / received message |\n| `Blax\\Mail\\Models\\MailRecipient` | `mail_recipients` | Normalized address-per-row for indexed `forSubject` lookups |\n| `Blax\\Mail\\Models\\MailAttachment` | `mail_attachments` | Filename, mime, size, storage path |\n| `Blax\\Mail\\Models\\MailEvent` | `mail_events` | Audit log: `Queued` / `Sent` / `Opened` / `Clicked` / … |\n\n`MailMessage` also exposes:\n\n```php\n$message-\u003emailbox;      // BelongsTo Mailbox\n$message-\u003erecipients;   // HasMany MailRecipient\n$message-\u003eattachments;  // HasMany MailAttachment\n$message-\u003eevents;       // HasMany MailEvent (audit timeline)\n$message-\u003esubject;      // MorphTo — resolves the polymorphic subject if set\n$message-\u003ethread();     // Whole thread as a Collection\n$message-\u003eparent();     // Immediate parent in the thread, or null\n$message-\u003eisInbound();  // bool\n$message-\u003eisOutbound(); // bool\n$message-\u003emarkRead();   // status → Read\n```\n\n### Swapping a model\n\n```php\n// config/blax-mail.php\n'models' =\u003e [\n    'mail_message' =\u003e \\App\\Models\\MyMailMessage::class,   // extends Blax\\Mail\\Models\\MailMessage\n],\n```\n\nThe package resolves every model via `config('blax-mail.models.X')`, so a subclass slots in without touching the core code.\n\n## Enums\n\n`Blax\\Mail\\Enums\\MailDirection`:\n\n| Case | Value |\n|---|---|\n| `Outbound` | `outbound` |\n| `Inbound` | `inbound` |\n\n`Blax\\Mail\\Enums\\MailStatus`:\n\n| Case | Value | Notes |\n|---|---|---|\n| `Queued` | `queued` | Outbound — persisted, awaiting SMTP |\n| `Sending` | `sending` | Outbound — `SendMailJob` is running |\n| `Sent` | `sent` | Outbound — SMTP accepted |\n| `Delivered` | `delivered` | Outbound — confirmed delivered (provider-dependent) |\n| `Bounced` | `bounced` | Outbound — provider reported bounce |\n| `Failed` | `failed` | Outbound — all retries exhausted |\n| `Received` | `received` | Inbound — fresh from poller |\n| `Read` | `read` | Inbound — `markRead()` was called |\n\n`Blax\\Mail\\Enums\\MailEventType` (audit log entries): `Queued`, `Sent`, `Delivered`, `Bounced`, `Complaint`, `Failed`, `Opened`, `Clicked`, `Received`, `Threaded`.\n\n## Console commands\n\n```bash\n# Poll every enabled mailbox once (typically invoked by the scheduler).\nphp artisan blax-mail:poll\n\n# Restrict to one mailbox (matches by id, email, or name):\nphp artisan blax-mail:poll --mailbox=support@example.com\n\n# Hard-delete soft-deleted messages older than retention.purge_days.\nphp artisan blax-mail:cleanup\n```\n\n## Scheduler\n\nThe 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.\n\nDefaults:\n\n```\ncron expression       * * * * *      every minute\nwithout overlapping   true           mutex prevents queue-up\n```\n\nOverride per environment:\n\n```dotenv\nBLAX_MAIL_POLL_CRON=\"*/5 * * * *\"          # poll every 5 minutes\nBLAX_MAIL_SCHEDULE_NO_OVERLAP=false        # allow parallel polls\nBLAX_MAIL_SCHEDULE_ENABLED=false           # disable auto-schedule, register manually\n```\n\nIf you set `BLAX_MAIL_SCHEDULE_ENABLED=false`, register the command yourself:\n\n```php\n// routes/console.php\nSchedule::command('blax-mail:poll')-\u003eeveryTenMinutes()-\u003ewithoutOverlapping();\n```\n\n## Extending\n\n### React to inbound mail\n\n```php\nuse Blax\\Mail\\Events\\InboundMailReceived;\n\nEvent::listen(InboundMailReceived::class, function (InboundMailReceived $event) {\n    // $event-\u003emessage       — Blax\\Mail\\Models\\MailMessage (already persisted)\n    // $event-\u003ethreadParent  — ?MailMessage (the matched outbound, or null)\n\n    // Typical pattern: file the inbound onto your own domain pivot.\n    // Walk threadParent → meta.app_mail_id → your domain Mail row, then clone\n    // its M:N subject linkage onto the inbound so it surfaces on every\n    // entity feed the original outbound was filed under.\n});\n```\n\n### Custom threading\n\n```dotenv\nBLAX_MAIL_AUTO_THREAD=false\n```\n\nThen subscribe to `InboundMailReceived` and set `thread_root_id` / `parent_id` yourself.\n\n### Custom transport / storage\n\nBind your own implementation:\n\n```php\n// AppServiceProvider::register\n$this-\u003eapp-\u003esingleton(\\Blax\\Mail\\Contracts\\Dispatcher::class, MyDispatcher::class);\n$this-\u003eapp-\u003esingleton(\\Blax\\Mail\\Contracts\\Poller::class, MyPoller::class);\n```\n\nBoth contracts have one method (`dispatch(OutboundMail): MailMessage`, `poll(Mailbox): int`).\n\n## Security\n\nPlease report vulnerabilities by email: **office@blax.at**. We'll acknowledge within 72 hours.\n\n## Credits\n\n- [Fabian Wagner](https://github.com/fabianwagner)\n- [All Contributors](../../contributors)\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n\n## Star History\n\n\u003ca href=\"https://www.star-history.com/?repos=blax-software%2Flaravel-mail\u0026type=date\u0026legend=top-left\"\u003e\n \u003cpicture\u003e\n   \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/chart?repos=blax-software/laravel-mail\u0026type=date\u0026theme=dark\u0026legend=top-left\" /\u003e\n   \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/chart?repos=blax-software/laravel-mail\u0026type=date\u0026legend=top-left\" /\u003e\n   \u003cimg alt=\"Star History Chart\" src=\"https://api.star-history.com/chart?repos=blax-software/laravel-mail\u0026type=date\u0026legend=top-left\" /\u003e\n \u003c/picture\u003e\n\u003c/a\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblax-software%2Flaravel-mail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblax-software%2Flaravel-mail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblax-software%2Flaravel-mail/lists"}