Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Mail with Mailgun Postmark and SES

By Kokil Thapa | Last reviewed: September 2026

Laravel Mail with Mailgun Postmark and SES is how most production Laravel applications send password resets, order receipts, booking confirmations, and client notifications without running their own SMTP server. Whether you deploy Laravel 13 on PHP 8.3+ or maintain Laravel 12 on PHP 8.2, all three providers plug into the same Mail facade and Mailable classes through Symfony Mailer transports. I've wired this stack on API-driven Laravel projects and legal-tech portals where a failed reset email blocks real users from completing a workflow. The goal is not to pick a winner in abstract—it's to configure each provider correctly, queue outbound mail, and know when to switch.

How does Laravel Mail work with Mailgun, Postmark, and SES?

Laravel's mail layer sits on top of Symfony Mailer. You write Mailable classes; Laravel resolves a transport from config/mail.php based on MAIL_MAILER. Each provider ships a dedicated transport that translates Laravel's message object into that vendor's HTTP API call. That separation matters: business logic lives in your app, deliverability tooling lives at the provider.

On a typical production Laravel application, the flow looks like this:

  1. A controller, job, or listener calls Mail::to($user)->send(new OrderConfirmed($order)).
  2. If ShouldQueue is implemented, the serialized Mailable lands on your queue driver (Redis 8.10 is a common choice).
  3. A queue worker rebuilds the Mailable and hands it to the configured transport.
  4. The transport POSTs to Mailgun, Postmark, or SES and returns a message ID.
  5. The provider handles bounces, complaints, and delivery events via webhooks you can log or act on.
Laravel Mail Transport ArchitectureControllerJob / EventMailableBlade viewQueue WorkerRedis driverSymfonyMailerMailgun APIEU or US regionPostmark APIServer tokenAWS SES APIIAM credentialsRecipient InboxSPF + DKIM + DMARC verified domain
Laravel Mail with Mailgun Postmark and SES — one Mailable class, three interchangeable HTTP transports

Your config/mail.php defines the mailers array. Laravel 13 ships with stubs for all three providers. The default mailer reads from the environment:

// config/mail.php (excerpt)
'default' => env('MAIL_MAILER', 'log'),

'mailers' => [
    'mailgun' => [
        'transport' => 'mailgun',
    ],
    'postmark' => [
        'transport' => 'postmark',
    ],
    'ses' => [
        'transport' => 'ses',
    ],
    'log' => [
        'transport' => 'log',
        'channel' => env('MAIL_LOG_CHANNEL'),
    ],
],

Keep MAIL_MAILER=log in local development unless you intentionally test against a sandbox domain. For staging, use each provider's sandbox or a dedicated test subdomain so you never pollute production sender reputation. The Laravel 13 mail documentation covers the full configuration surface, including failover mailers introduced in recent releases.

How do you configure Mailgun for Laravel Mail?

Mailgun fits teams that want flexible domain routing, EU data residency, and detailed event logs without AWS lock-in. Setup is straightforward once DNS is correct—most production incidents I've debugged trace back to missing DKIM, not application code.

Install the package and set environment variables

Install the official transport via Composer 2.10:

composer require symfony/mailgun-mailer symfony/http-client

Then configure .env:

MAIL_MAILER=mailgun
MAIL_FROM_ADDRESS=noreply@yourdomain.com
MAIL_FROM_NAME="${APP_NAME}"

MAILGUN_DOMAIN=mg.yourdomain.com
MAILGUN_SECRET=key-xxxxxxxxxxxxxxxx
MAILGUN_ENDPOINT=api.mailgun.net

For EU-hosted accounts, set MAILGUN_ENDPOINT=api.eu.mailgun.net. The endpoint must match where you created the domain in the Mailgun dashboard; mixing US credentials with an EU endpoint produces opaque 401 errors that look like Laravel bugs but are really region mismatches.

Verify DNS records

Mailgun provides TXT records for SPF, CNAME records for DKIM, and optionally a DMARC policy. Add them at your DNS host before sending production traffic. A common mistake on client projects is verifying the root domain while sending from a subdomain—pick one sending domain and align MAIL_FROM_ADDRESS with it.

Send a test Mailable

php artisan make:mail WelcomeUser --markdown=emails.users.welcome

// app/Mail/WelcomeUser.php
public function envelope(): Envelope
{
    return new Envelope(
        subject: 'Welcome to ' . config('app.name'),
    );
}

Trigger it from Tinker or a feature test:

Mail::to('you@example.com')->send(new WelcomeUser($user));

On booking systems like Adventure Third Pole Trek, confirmation emails must queue immediately after payment—never send synchronously inside the HTTP request that handles the gateway callback. Wrap the Mailable with ShouldQueue and ensure your worker processes the mail queue. Patterns from Laravel payment integrations apply directly here: the payment succeeds even if the mail worker is briefly down, because the job retries.

How do you set up Postmark and AWS SES in Laravel?

Postmark and SES serve different operational profiles. Postmark optimises for transactional-only sending with excellent default deliverability and minimal configuration. SES is the cost leader at scale and integrates naturally when your Laravel app already runs on AWS EC2 or RDS.

Postmark configuration

Install the transport:

composer require symfony/postmark-mailer symfony/http-client

Environment variables:

MAIL_MAILER=postmark
POSTMARK_TOKEN=your-server-api-token

Postmark uses a Server model: one server token per application environment. Create separate Postmark servers for staging and production—never share tokens. Verify your sender signature or domain in the Postmark dashboard, then add the DKIM CNAME records they provide.

Postmark rejects mail that looks promotional if your server is configured for transactional traffic. That discipline is useful: it forces you to route newsletters through a separate provider or server, which protects password-reset deliverability. For a deeper provider comparison, see the related write-up on AWS SES vs SendGrid vs Postmark for Laravel Mail.

AWS SES configuration

SES requires the AWS SDK and Symfony transport:

composer require aws/aws-sdk-php symfony/amazon-mailer

Environment variables:

MAIL_MAILER=ses
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=ap-south-1

New SES accounts start in sandbox mode—you can only send to verified addresses until you request production access through the AWS console. For Laravel apps deployed on AWS EC2 with RDS, use an IAM role attached to the instance instead of long-lived access keys in .env. The SDK picks up instance metadata automatically when keys are omitted.

Verify your domain in SES, publish the three DKIM CNAME records, and enable SPF alignment through the included MAIL FROM subdomain. If you already send from the same domain via another provider, remove old DKIM records before adding SES's—duplicate DKIM selectors cause intermittent authentication failures that are painful to diagnose.

DNS Verification Checklist1. Add DomainProvider dashboard2. SPF TXTInclude provider3. DKIM CNAMETwo or three records4. DMARC TXTStart with noneWait for DNS PropagationUsually 5 to 30 minutes; TTL dependentSend Test MailCheck headers for d= passEnable ProductionRemove sandbox limits
Domain authentication steps shared by Mailgun, Postmark, and SES before Laravel sends production mail

Failover between providers

Laravel supports a failover mailer that tries transports in sequence. This is useful during provider outages:

// config/mail.php
'failover' => [
    'transport' => 'failover',
    'mailers' => ['postmark', 'ses', 'log'],
],

// .env
MAIL_MAILER=failover

Failover is not a substitute for monitoring. Log the final transport used and alert when the primary fails. On systems I maintain with GitLab CI deploy pipelines, mail credentials live in environment-specific secrets, never in the repository.

Which provider should you choose for Laravel Mail in production?

The right choice depends on volume, infrastructure, compliance, and who owns deliverability when something breaks. All three work with the same Mailable code—your decision is operational, not architectural.

CriteriaMailgunPostmarkAWS SES
Best forMulti-region apps, dev-friendly logs, flexible routingTransactional-only apps prioritising inbox placementHigh volume, AWS-native stacks, cost sensitivity
Typical cost at 50k emails/month~USD 35 / Rs 4,700~USD 50 / Rs 6,700~USD 5 / Rs 670 (plus data transfer)
EU / GDPR data residencyEU endpoint availableEU servers availableRegion-selectable (e.g. eu-west-1)
Setup complexityModerate — domain + API keyLow — server token + DNSHigher — IAM, sandbox exit, SNS for bounces
Marketing + transactional mixSupported (separate domains recommended)Transactional only on one serverBoth, but reputation management is on you
Bounce handlingWebhooks + event APIWebhooks + detailed dashboardSNS notifications → Lambda or HTTP endpoint

Practical guidance from production work:

  • Choose Postmark when you send mostly transactional mail—password resets, invoice PDFs, appointment confirmations—and want the fastest path to strong deliverability without becoming an email engineer.
  • Choose SES when you already run on AWS, send high volume (100k+ monthly), and have someone who can manage SNS bounce pipelines and IAM policies.
  • Choose Mailgun when you need EU routing, granular event logs, or a single API that also handles inbound parsing and route rules.
Provider Decision TreeNew Laravel App?Already on AWS?EC2, RDS, S3Volume over 100k?Emails per monthTransactional only?No newslettersAWS SESLow cost at scaleAWS SESHigh volume pathPostmarkBest deliverabilityMailgun — EU routing or inbound parseNeed flexible domain rules
Choosing between Mailgun, Postmark, and SES for Laravel Mail based on infrastructure and volume

For a legal-tech portal like Mijar Law Associates, I default to Postmark or Mailgun because clients can read bounce dashboards without learning AWS. For Nepal Gift Card—a high-volume digital delivery platform—SES or Mailgun's pay-as-you-go pricing wins on unit economics. None of these choices require rewriting your Mailable layer; you change env vars, DNS, and webhook routes.

How do you queue, test, and debug transactional email in Laravel?

Configuration is half the job. Production reliability comes from queuing, testing, and handling bounces so you do not mail dead addresses for months.

Always queue outbound mail

Implement ShouldQueue on every Mailable that is not safety-critical and instant:

use Illuminate\Contracts\Queue\ShouldQueue;

class OrderConfirmed extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public Order $order) {}
}

Run a dedicated worker for the mail queue in production:

php artisan queue:work redis --queue=mail,default --tries=3 --backoff=60

Supervise that process with systemd or your process manager. After deploy, reload workers—stale workers are a recurring cause of "mail worked yesterday" reports on support and maintenance contracts.

Local and CI testing

Use MAIL_MAILER=array or log in PHPUnit. Assert mail was sent with:

Mail::fake();

Mail::assertSent(OrderConfirmed::class, function ($mail) use ($order) {
    return $mail->order->id === $order->id;
});

For local preview, Postmark's sandbox server and Mailgun's test mode accept API calls without delivering to real inboxes. Mailtrap and similar inbox simulators are fine for UI review but do not validate DNS authentication—you still need a staging send through the real provider before launch.

When debugging payload issues, paste JSON webhook bodies into the JSON formatter tool to inspect bounce and complaint structures quickly.

Handle bounces and suppressions

Each provider sends webhook events for bounces, complaints, and unsubscribes. Create a signed route and mark users undeliverable:

// routes/web.php
Route::post('/webhooks/postmark', [PostmarkWebhookController::class, 'handle']);

// Disable mail to hard-bounced addresses in your User model
public function scopeMailable($query)
{
    return $query->whereNull('email_bounced_at');
}

Continuing to send to hard bounces damages domain reputation for every mail type—not just the one that bounced. Wire webhook handlers in the same sprint as mail integration, not six months later when Gmail starts spam-foldering password resets.

Production Mail Monitoring LoopQueue WorkerSends MailableProvider APIDelivery attemptRecipientInbox or spamWebhook: Bounce / Complaint / DeliverySigned POST to Laravel routeSuppress AddressSet email_bounced_atAlert OpsSlack or log channelMetricsTrack send rateHealthy domain reputation over time
Webhook-driven bounce handling protects sender reputation for Laravel Mail with Mailgun Postmark and SES

Structured logging helps. Log the provider message ID alongside your local correlation ID (order ID, booking ID). When a user claims they never received a reset link, you can trace the exact delivery event in the provider dashboard within seconds.

Align mail architecture with broader application patterns from modern Laravel architecture best practices—keep Mailables thin, put attachment generation in actions or services, and never embed raw HTML strings in controllers. For apps exposing outbound notifications via API, document mail triggers alongside endpoints in your Laravel API best practices guide internally so frontend teams know which actions send email.

Email also intersects with SEO for Laravel sites in one specific way: marketing pages should not share the transactional sending domain. A promotional blast that triggers spam complaints can drag password-reset mail into junk folders on the same domain. Use separate subdomains—tx.yourdomain.com for transactional, news.yourdomain.com for campaigns.

Hosting choice affects SES latency but rarely matters for Mailgun or Postmark. If you compare providers and infrastructure together, the AWS vs DigitalOcean vs Hetzner for Laravel hosting analysis helps frame total cost. For enterprise apps with strict audit trails, pair mail logging with database event records using patterns from database migrations best practices so you have a local copy of sent notifications.

On platforms that also push real-time updates—booking status, chat, admin alerts—mail complements rather than replaces broadcasting. See Laravel broadcasting with Reverb for the websocket side; email remains the durable fallback when the user is offline.

If you build custom admin panels with Filament or Livewire, wire a "resend notification" action that re-queues the same Mailable rather than duplicating template logic. The Laravel Livewire tutorial covers component patterns that map cleanly to admin resend buttons. For larger builds, enterprise application development engagements typically include mail as part of the notification matrix from day one—not as a post-launch patch.

Reference the official Mailgun documentation for inbound routing if your app parses replies, and the AWS SES developer guide for SNS bounce subscription setup. Both are authoritative when Laravel's abstraction layer hides the details you need during incident response.

Key Takeaways

  • Laravel Mail with Mailgun Postmark and SES uses the same Mailable classes—switching providers is an env and DNS change, not a rewrite.
  • Verify SPF, DKIM, and DMARC on your sending domain before production traffic; most deliverability failures are DNS problems, not PHP bugs.
  • Queue every non-instant Mailable with ShouldQueue and supervise workers across deploys.
  • Choose Postmark for transactional simplicity, SES for AWS-native cost at scale, Mailgun for EU routing and flexible domain rules.
  • Handle bounces via webhooks immediately—suppress hard-bounced addresses to protect domain reputation.
  • Keep transactional and marketing mail on separate subdomains so campaigns never poison password-reset deliverability.

People Also Ask

Can I use multiple email providers at the same time in Laravel?

Yes. Laravel's failover mailer tries transports in order until one succeeds. You can also route different Mailables to different mailers by calling Mail::mailer('postmark')->send(...) explicitly. Some teams send transactional mail through Postmark and marketing campaigns through Mailgun on a separate domain.

Does Laravel 13 require a separate package for SES?

You need symfony/amazon-mailer and aws/aws-sdk-php installed via Composer. Laravel 13 includes the configuration stubs, but the AWS SDK is not bundled by default. Provide credentials through IAM roles on EC2 or through standard AWS environment variables.

Why do my Laravel emails go to spam with Mailgun or SES?

Usually because DKIM or SPF is misconfigured, MAIL_FROM_ADDRESS does not match the verified domain, or you are sending from a fresh domain without warmed reputation. Check the provider's authentication dashboard, send a test to mail-tester.com, and confirm you queue mail rather than blasting thousands of messages synchronously from a loop.

Should I use SMTP instead of the Mailgun, Postmark, or SES API?

API transports are preferred in production. SMTP adds connection overhead, lacks rich error responses, and is harder to retry intelligently from queued jobs. SMTP remains acceptable for local development with Mailhog or Mailpit, but production Laravel apps should use the HTTP transports bundled with Symfony Mailer.

Ship reliable Laravel Mail in production

Laravel Mail with Mailgun Postmark and SES gives you production-grade transactional email without maintaining SMTP infrastructure. Pick the provider that matches your hosting and volume, verify DNS before the first real send, queue everything that can wait, and wire bounce webhooks in the same release as your first Mailable. The application code stays boring and portable—that is the point.

Need mail integrated into a booking flow, client portal, or eCommerce checkout? Contact us to scope Laravel notification architecture, provider setup, and production monitoring. For related reading, explore the Laravel blog archive or review shipped work in the portfolio.

Frequently Asked Questions

Laravel Mail with Mailgun, Postmark, and SES is how production Laravel apps send password resets, receipts, and notifications without running your own SMTP server—all three plug into the same Mail facade through Symfony Mailer HTTP transports.

You write Mailable classes; Laravel resolves a transport from config/mail.php based on MAIL_MAILER. Each provider's transport converts Laravel's message object into an HTTP API call to Mailgun, Postmark, or SES. Business logic stays in your app; deliverability tooling lives at the provider. If the Mailable implements ShouldQueue, a worker rebuilds it and hands it to the transport, which returns a provider message ID while webhooks report bounces and complaints.

Install symfony/mailgun-mailer and symfony/http-client via Composer 2.10, then set MAIL_MAILER=mailgun with MAILGUN_DOMAIN, MAILGUN_SECRET, and MAILGUN_ENDPOINT in .env. Align MAIL_FROM_ADDRESS with your verified sending domain, add Mailgun's SPF TXT, DKIM CNAME, and optional DMARC records at your DNS host, and use api.eu.mailgun.net for EU-hosted accounts. Most production failures I've debugged trace to missing DKIM, not application code.

Run composer require symfony/postmark-mailer symfony/http-client, set MAIL_MAILER=postmark and POSTMARK_TOKEN to your server API token. Create separate Postmark servers for staging and production—never share tokens. Verify your sender signature or domain, add Postmark's DKIM CNAME records, and keep the server transactional-only. Postmark rejects promotional-looking mail on transactional servers, which protects password-reset deliverability.

Install aws/aws-sdk-php and symfony/amazon-mailer, then set MAIL_MAILER=ses with AWS credentials and AWS_DEFAULT_REGION. New SES accounts start in sandbox mode until you request production access. Verify your domain, publish SES's three DKIM CNAME records, and enable SPF via the MAIL FROM subdomain. On EC2, attach an IAM role instead of storing long-lived keys in .env—the SDK reads instance metadata automatically.

Choose Postmark when you send mostly transactional mail and want strong deliverability with minimal setup. Choose SES when you already run on AWS, send high volume, and can manage SNS bounce pipelines and IAM policies. Choose Mailgun when you need EU routing, granular event logs, or flexible domain routing without AWS lock-in. All three use identical Mailable classes—you change env vars, DNS, and webhooks, not application architecture.

At roughly 50,000 emails monthly: Mailgun ~USD 35 (Rs 4,700), Postmark ~USD 50 (Rs 6,700), AWS SES ~USD 5 (Rs 670) plus data transfer.

Yes—queue every non-instant Mailable by implementing ShouldQueue, especially after payment callbacks on booking systems.

Use MAIL_MAILER=log unless you intentionally test against a provider sandbox or dedicated test subdomain.

Laravel supports a failover mailer in config/mail.php that tries transports in sequence, for example postmark then ses then log. Set MAIL_MAILER=failover in .env. Failover helps during provider outages but is not a substitute for monitoring—log which transport succeeded and alert when the primary fails. Store mail credentials in environment-specific secrets in your CI pipeline, never in the repository.

The most common cause is a region mismatch: US credentials paired with the EU endpoint or vice versa. MAILGUN_ENDPOINT must match where you created the domain in the Mailgun dashboard—api.mailgun.net for US, api.eu.mailgun.net for EU. These opaque 401 responses look like Laravel bugs but are really endpoint misconfiguration. I've seen teams waste hours debugging Mailable code when the fix is a single .env line.

In PHPUnit, set MAIL_MAILER to array or log and use Mail::fake() with Mail::assertSent() to verify the correct Mailable was dispatched with expected data. For local preview, Postmark's sandbox server and Mailgun's test mode accept API calls without delivering to real inboxes. Inbox simulators like Mailtrap help review templates but do not validate DNS authentication—you still need a staging send through the real provider before launch.

Each provider sends webhook events for bounces, complaints, and unsubscribes. Create a signed route—Route::post('/webhooks/postmark', ...)—and mark hard-bounced users undeliverable, for example with a scopeMailable query that excludes records where email_bounced_at is set. Wire webhook handlers in the same sprint as mail integration, not months later. Continuing to send to hard bounces damages domain reputation for every mail type on that domain.

A promotional blast that triggers spam complaints can drag password-reset mail into junk folders when both share the same sending domain. Use separate subdomains—tx.yourdomain.com for transactional, news.yourdomain.com for campaigns. Postmark enforces transactional-only servers, which reinforces this split. Marketing pages should not share the transactional sending domain even though Laravel Mail configuration technically allows it.

All three require domain authentication before production sending. Mailgun needs SPF TXT, DKIM CNAME, and optionally DMARC. Postmark requires sender signature or domain verification plus DKIM CNAME records. SES needs domain verification, three DKIM CNAME records, and SPF alignment through the MAIL FROM subdomain. If switching providers on the same domain, remove old DKIM records before adding new ones—duplicate selectors cause intermittent authentication failures that are painful to diagnose.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: