Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mailbox For Laravel Laravel Package

redberry/mailbox-for-laravel

Redberry Mailbox for Laravel adds inbound email handling to your app. Receive messages via webhook drivers, store them with Eloquent models, manage attachments and addresses, and process incoming mail using familiar Laravel conventions.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and run its one-time installer — that command is what publishes the config, ships the prebuilt dashboard assets, and runs the migrations for the default SQLite store:

composer require redberry/mailbox-for-laravel --dev
php artisan mailbox:install

Then point your local mailer at the package's transport. The transport is registered as mailbox and intercepts outgoing mail instead of handing it to SMTP:

MAIL_MAILER=mailbox

Open /mailbox (the prefix comes from config('mailbox.path', 'mailbox')) and send any mailable from your app — a registration email, a password reset, an invoice. You should see it appear in the dashboard within the polling interval, fully rendered, with attachments and inline images resolved.

The first use case to internalize is "look before you assert." Send the real mailable through the app, inspect exactly what the recipient would see in the dashboard (HTML, plain-text fallback, headers, attachment names and sizes), then turn that observation into a test using the packaged testing utilities. That loop — dashboard to verify, test to lock in — is the core day-to-day workflow, and it is why the package is more useful than a generic Mail::fake(): Mail::fake() tells you a mailable was queued; this tells you what it looked like.

Where to look first when something is unclear:

  • config/mailbox.php for driver, path, gate, polling, retention, and attachment disk settings.
  • MailboxController if you need to understand why a request returns Blade versus JSON (it branches on $request->wantsJson()).
  • src/Testing/ for InteractsWithMailbox, the Mailbox facade, MailboxAssertions, and PendingMailboxMessageAssertion.
  • The viewMailbox gate if the dashboard 403s.

Implementation Patterns

Local development inbox. Keep MAIL_MAILER=mailbox in .env.local (or a dedicated local mailer entry) so nobody accidentally emails real customers from a dev machine. Because the capture happens at the transport layer, anything that goes through Laravel's mailer — notifications, Mailable classes, Mail::raw() — lands in the dashboard without per-mailable changes. Pair this with retention so the SQLite file does not grow forever, and schedule a purge:

Schedule::command('mailbox:clear --outdated')->daily();

Test assertions. This is the highest-leverage part of the package. Add the trait once in your Pest TestCase or per-file, and the mailbox is cleared automatically before each test:

uses(InteractsWithMailbox::class);

it('sends the welcome email', function () {
    $this->post('/register', [...])->assertRedirect();

    Mailbox::assertSentTo('[email protected]');
    Mailbox::assertSentCount(1);
    Mailbox::firstSent()
        ->assertHasSubject('Welcome aboard')
        ->assertSeeInHtml('Thanks for signing up')
        ->assertHasHeader('X-Campaign', 'onboarding');
});

Reach for the collection-level assertions (assertSent, assertNotSent, assertNothingSent, assertSentCount, assertSentTo, assertNotSentTo, plus the sent() and firstSent() accessors) to express routing intent, and the per-message fluent assertions (assertFrom, assertHasTo, assertHasSubject, assertSeeInHtml, assertHasAttachment, assertHasHeader) to express content intent. Split those into separate it() blocks in a describe() — a failing subject line and a failing recipient are different bugs and should not be conflated in one red test.

Assert on rendered output, not on the mailable object. The whole point of capturing "fully rendered messages" is that you catch Blade errors, missing translated strings, broken markdown components, and wrong layout selection. assertSeeInHtml('...') is a stronger test than inspecting view data, because it fails when the template silently renders nothing.

Attachments and inline images. Assert the attachment exists and the inline image resolves through Support\CidRewriter, which maps cid: references to the attachment store. If your mailable embeds a logo with $message->embed(...), the dashboard should show it inline; a broken image in the dashboard almost always means the attachment disk/path config is inconsistent between the message store and attachment store, not that the mailable is wrong.

Authorization for shared/staging environments. Locally the dashboard is convenient; anywhere else it is an information leak. Define the viewMailbox gate explicitly in a service provider before deploying to staging, and keep it closed by default elsewhere. Route access is gated by the viewMailbox gate through the mailbox.authorize middleware — treat "no gate defined" as "do not expose this route publicly."

Custom storage drivers. If you outlive SQLite (multi-node staging, shared volume), implement the pair: Contracts\MessageStore (10 methods) and Contracts\AttachmentStore (9 methods), then register the message store through config('mailbox.store.resolvers') — singular store, and not Manager::extend(). Bind the matching AttachmentStore in the same provider; otherwise the package silently falls back to DatabaseAttachmentStore and reintroduces a DB dependency you were trying to remove. Mirror tests/Unit/Contracts/MessageStoreContractTest.php and AttachmentStoreContractTest.php — those dataset-driven contract tests are how you prove your driver behaves like the built-ins.

Dashboard extension work. If you touch the Vue app, remember it is a standalone Vue 3 + TypeScript app, not Inertia. Build URLs through mailboxUrl() from resources/js/lib/mailboxStore.ts so the configurable path prefix keeps working, keep state in that shared reactive store rather than prop-drilling, and let Vite emit to public/vendor/mailbox/. Never import from the host app's frontend — the isolation is the feature.

Gotchas and Tips

  • mailbox:install is not optional and not idempotent-by-accident. Skipping it means no published assets and a dashboard that renders a blank page or 404s on JS/CSS. If you change mailbox.path after installing, re-check asset URLs rather than assuming it just works.
  • Config caching will bite you. mailbox.store.driver, mailbox.path, and the attachment disk are read through config(); after changing them in a cached environment run php artisan config:clear. This is also why the package forbids env() outside config/mailbox.php — respect that in your own code and you avoid the "works locally, broken in CI" class of bug.
  • Custom driver config is per-driver nested. Driver options live under mailbox.store.{driver_name}, so a driver named redis reads config('mailbox.store.redis'). Putting keys at the top level of store is the most common misconfiguration.
  • Driver pairs must match. A JSON-file MessageStore paired with the default DB AttachmentStore gives you attachments that outlive (or vanish before) their messages. Bind both or bind neither.
  • Don't hand-roll purge logic. CaptureService cascades attachment deletion using idsOlderThan before purging messages, precisely so orphan blobs don't accumulate. Reimplementing that inside your driver will drift.
  • Dedup is by RFC Message-ID. findIdByMessageId gives the write path idempotency; retried sends or double-dispatched jobs will not produce two dashboard entries. Don't assume every Mail::send() adds a row.
  • paginate() must be newest-first. The dashboard depends on it; getting this backwards is a subtle, easy-to-miss contract violation that only shows up with more than one page of mail.
  • Testing trait clears state — rely on it, don't fight it. InteractsWithMailbox clears the mailbox before each test, so you do not need Mailbox::assertNothingSent() as a defensive preamble. Under Testbench the package configures in-memory SQLite automatically, which means tests are fast and isolated but also that nothing persists between tests — never assert across test boundaries.
  • Assertion ergonomics. firstSent() returns a PendingMailboxMessageAssertion; chaining is fluent and each method returns $this. If you need to assert on several messages, use sent() and iterate rather than calling firstSent() repeatedly — the latter always means index 0.
  • Dashboard HTML is rendered output and must stay sanitized. Do not "temporarily" v-html untrusted mail bodies in the Vue app to debug something. This is the one place in the package where a convenience shortcut becomes a stored-XSS vector on a route your teammates can reach.
  • Zero dependents, 126 stars. The surface is small and the maintainers' own rule files are the best spec you have, but the wider ecosystem has not battle-tested every edge. When behavior and documentation disagree, read the source (MailboxTransport → MessageNormalizer → CaptureService → MessageStore) rather than assuming Laravel-core semantics.
  • Keep it out of production mail paths. This is a local inbox, not an observability platform. Ship it as a require-dev dependency, gate the route, and if you find yourself writing a Horizon-style retention policy for it, you probably want a real transactional email provider instead.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource