Guidance for AI coding agents when working in this repository.
webteractive/mailulator — self-hosted, Laravel-native email testing. One Composer package ships both sides:
- Receiver — ingest API + Vue 3 SPA (
/mailulator) + isolated DB. - Driver — Symfony Mailer transport registered as
mailulator.
Either side is independently disable-able via MAILULATOR_RECEIVER_ENABLED / MAILULATOR_DRIVER_ENABLED. Two deployment shapes:
- In-app — receiver and driver both enabled in the same host app, no
MAILULATOR_URL. Driver bypasses HTTP and writes directly to theDefaultinbox viaStoreIncomingEmail. - Standalone — dedicated receiver app; one or more sender apps post to it over HTTP using
MAILULATOR_URL+ per-inboxMAILULATOR_TOKEN.
Horizon/Telescope-style:
MailulatorServiceProvider(package) registers routes, config, migrations, transport, broadcast channels.MailulatorApplicationServiceProvider(abstract) — host app extends it inapp/Providers/MailulatorServiceProvider.phpto define the gate,canViewInbox, andmanageclosures.- Static
Mailulator::configurator. Default gate: local environment only. mailulator:installpublishes the stub provider + config + assets and seeds a protectedDefaultinbox.
Frontend: Vue 3 + Pinia + Vue Router + shadcn-vue (reka-ui) + Tailwind (HSL tokens). Built with Vite 8; compiled dist/ is committed and published to public/vendor/mailulator.
- No SMTP. Cross-app ingest is HTTP only (
POST /api/emails); in-app mode bypasses HTTP and persists in-process viaStoreIncomingEmail. - Token → inbox. The bearer token alone routes mail. Stored as
sha256hash. One token, one inbox. - Isolated
mailulatorDB connection. Never touches the host app's primary DB. Registered inregister()(notboot()) so migrations resolve. All migrations pinprotected $connection = 'mailulator'. - Default inbox is protected. Cannot be renamed or deleted. Last remaining inbox cannot be deleted.
- Polling default, broadcast opt-in.
MAILULATOR_REALTIME=pollingis dependency-free.broadcastrequires Reverb/Pusher in the host app. - Attachments on a configurable disk (
MAILULATOR_ATTACHMENTS_DISK), streamed viaStorage::download— never a public URL.
inboxes— name, hashedapi_key,retention_days,is_default,settings(JSON),last_used_at.emails—inbox_id, from/to/cc/bcc, subject, html/text bodies,headersJSON,read_at.attachments—email_id, filename, mime_type, size, disk, path.
- Pint runs on every commit (
vendor/bin/pint --dirty). PHPStan level 4. - No FQCN inline —
useimports at the top. - No comments on obvious code; docblocks only for non-obvious params or array shapes.
Inbox::COLOR_REGEXis the single source of truth for color validation.- Mass-assignment:
$fillable = ['name', 'retention_days', 'settings']only.api_key,is_default,last_used_atrequireforceCreate/forceFill. - Tests: Pest 3 + Testbench.
:memory:SQLite for bothtestingandmailulatorconnections.
composer dev— concurrent Vite + Testbench server (HMR).composer serve— Testbench server only (uses built assets).composer fresh— drops the workbench SQLite + rebuilds.composer test/composer analyse/composer format.npm run build— required before tagging ifresources/js/**orresources/css/**changed; CI guards against staledist/.
Mail::extend('mailulator', ...)must run inboot(), notregister().app.blade.phpreadspublic_path('vendor/mailulator/.vite/manifest.json'). Never use@vite()— that resolves to the host app's manifest.- The published gate defaults to "local env only." Non-local installs without a customized gate → 403 for everyone.