smtpfast/smtpfast-laravel
SMTPFast for Laravel integrates the SMTPFast sending service with Laravel's built-in mail system, adding a ready-made transport and simple configuration so you can send transactional email from any Laravel application.
This is the first release (v0.1.0) of a Laravel mail transport that replaces SMTP with SMTPfast's HTTP API. The value proposition is that nothing about your existing Mailables or Notifications changes — only the mailer.
Install and let Composer resolve Laravel 12/13 (the package requires PHP 8.2+):
composer require smtpfast/laravel
Publish the package config if the service provider exposes one (check the README for the exact tag):
php artisan vendor:publish --tag=smtpfast-config
Switch the default mailer in .env and add the key:
MAIL_MAILER=smtpfast
SMTPFAST_API_KEY=sk_live_...
MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="${APP_NAME}"
Clear cached config, since MAIL_MAILER is read from the config cache:
php artisan config:clear
Smoke test with an existing Mailable, or the fastest possible check:
Mail::raw('hello', fn ($m) => $m->to('[email protected]')->subject('SMTPfast test'));
Where to look first: the README's configuration section (env var names, webhook route/signature header), then config/mail.php to confirm the package registered a smtpfast entry in the mailers array. In the source, the transport/mailer class is the interesting file — it's where API errors are translated (or not) into mail exceptions. The webhook section is a separate concern and can be skipped entirely on day one.
First real use case: point staging at SMTPfast, keep log locally, and send a queued password-reset or receipt email. If that lands with correct From, Reply-To, attachments, and inline images, the migration is effectively done.
Keep the default mailer as the swap point. Set MAIL_MAILER=smtpfast and stop there. Existing Mail::to(...)->send(...), Notification::send(...), and MustVerifyEmail flows all keep working. Resist the urge to touch Mailables during the migration — that's how you lose the ability to A/B against your old provider.
Define a second named mailer for scoped routing. If you only want transactional mail on SMTPfast while marketing mail stays elsewhere, add both entries under mailers and route per message:
Mail::mailer('smtpfast')->to($user)->send(new Receipt($order));
// or on the Mailable
public function mailer(): string { return 'smtpfast'; }
Notifications honor ->mailer() inside the toMail() closure, so mixed-channel notifications can be pinned per-notification.
Always queue. The transport makes an outbound HTTP call, so an unqueued send inside a request adds the API's latency directly to your response time. Implement ShouldQueue on Mailables and notifications, and set $tries/$backoff deliberately — a retry is a second email unless the API supports idempotency keys (check the client's send signature).
Treat the singleton client as the escape hatch, not the norm. The package binds the SMTPfast PHP client as a singleton plus a facade. Use it for things the mail transport doesn't cover (account/suppression lookups, template or domain management). Inject it via the container rather than new-ing it, and wrap it behind your own thin interface so you can fake it in tests:
$this->app->bind(MailAuditContract::class, fn ($app) => new SmtpfastAudit($app->make(SMTPfast::class)));
Webhook handling: verify, then queue. Register the route yourself (typically in routes/api.php or via the package's route registration), verify the signature against the raw request body, return 200 immediately, and hand off to a job:
Route::post('/webhooks/smtpfast', SmtpfastWebhookController::class)
->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
Make the handler idempotent — key on the event ID and upsert, because providers redeliver. Store delivery/bounce events against your own messages table keyed by your own message ID, not the provider's, so a future provider swap doesn't invalidate history.
Testing. Mail::fake() assertions are unchanged and remain the right level for feature tests. If you need to test the transport path itself, Http::fake() the SMTPfast endpoint and assert the payload shape (recipients, From, attachments) — that's the only place the package is actually observable.
Environment matrix. log locally, array in CI, smtpfast in staging and production. Keeping MAIL_MAILER environment-driven means a single .env.example documents both paths and no code branches on environment.
MAIL_HOST / MAIL_PORT / MAIL_USERNAME / MAIL_PASSWORD are ignored but confusing. Leave them out of .env.example, or add a comment. Half of "it's not sending" reports in this class of package are a teammate who re-added MAIL_HOST and assumed it was used.php artisan config:clear after adding SMTPFAST_API_KEY; under Octane run php artisan octane:reload. If you rotate API keys per tenant at runtime via config([...]), be aware the singleton client may already be resolved with the old key — app()->forgetInstance(...) or rebind before sending.composer update in mixed monorepos. Check with composer why-not smtpfast/laravel before blaming the update itself.Symfony\Component\Mailer\Exception\*. Failures surface as HTTP/client exceptions from the SDK, not transport exceptions. Catch broadly around Mail::send() (or let the queue handle it via failed()), and log the API response body — bounce and rejection reasons usually live there, and they're the only way to distinguish "bad recipient" from "bad key".MAIL_FROM_ADDRESS being authoritative. Many API mail providers reject or rewrite From addresses on unverified domains. Verify the sending domain first; a 403 on first send is almost always this, not a bug in the package.v0.1.0 is pre-1.0. Pin ~0.1.0 rather than ^0.1, read the changelog before bumping, and keep the package behind a small internal adapter so reverting to SMTP/SES is a one-file change. Expect envelope changes (config keys, exception types) before 1.0.$request->all() after the JSON middleware has parsed and re-encoded the payload will fail verification with a confusing mismatch. Also keep the route out of the web middleware group or you'll get a 419.php artisan config:show mail → confirm the smtpfast mailer entry exists and the key is populated → send via Mail::raw in tinker → if that fails, hit the client directly with the facade to isolate transport vs. SDK → then look at the API response body.How can I help you explore Laravel packages today?