php elephant sticker
Photo by RealToughCandy.com on Pexels.com
Table of Contents

[Field-Ready Complete Guide] Designing Email Delivery in Laravel — Mailable, Queues, Delivery Failures, Deliverability, Template Management, Notifications, Tracking, and How to Write Accessible Emails

What you’ll learn (key takeaways)

  • How to split responsibilities between Laravel’s email foundations (Mailable / Notification) and how to think about template governance
  • Don’t send synchronously: a battle-tested pattern for queueing, retries, and idempotency to prevent “double sends”
  • Operational practices to reduce delivery failures (bounces) and spam classification (SPF/DKIM/DMARC, list hygiene)
  • Separating transactional vs marketing email, sending limits, and rate limiting
  • Logging/auditing, and caveats when handling tracking (click/open) from a privacy standpoint
  • Multipart HTML/text, subject lines, from/reply-to, and link design
  • Readable, universally understandable accessible email (headings, lists, non-color cues, alt text)

Intended readers (who benefits?)

  • Laravel beginner–intermediate engineers: you can send email, but production issues like double sends and nondelivery scare you
  • Tech leads / operators: you want lower failure rates and faster root-cause investigation
  • PM / CS: you need critical notifications (expiry, payments, passwords) to reliably reach users
  • Designers / writers / accessibility owners: you want HTML email standardized into a form that is consistently “readable and understandable”

Accessibility level: ★★★★★

HTML email varies widely by client and environment, so accessibility work pays off. This guide shows practical patterns: heading structure, short paragraphs, bulleted lists, specific link text, alt text, non-color-dependent information, and pairing with a plain-text version.


1. Introduction: The goal isn’t “sending” — it’s “arriving and being understood”

As an app grows, email naturally multiplies: signup confirmations, password resets, invoices, export completion, invitations, anomaly alerts, and more. But in production, problems beyond “send succeeded” become common: “it sent but didn’t arrive,” “it went to spam,” “double sends,” “links are unclear,” “mobile layout breaks,” etc.

So treat email not as a one-off feature, but as a design that includes operations. Laravel gives you solid primitives with Mailable and Notification; if you standardize delivery mechanics, logging, deliverability, templates, and accessibility, you can dramatically reduce incidents.


2. Mailable vs Notification: A clear rule for choosing

2.1 Mailable (a letter dedicated to email)

Use it when you want to:

  • Carefully craft HTML/text body and subject line as “an email”
  • Reuse the same template from multiple places
  • Centralize email-specific settings such as attachments, headers, reply-to, etc.

2.2 Notification (an abstraction for “notifications”)

Use it when you want to:

  • Expand beyond email (Slack, SMS, in-app notifications)
  • Manage everything under the concept of “notifications”
  • Support user notification preferences (email on, in-app off, etc.)

In real teams, a common split is:

  • Transactional email (signup, billing, reset): Mailable-first
  • User-preference / multi-channel: Notification-first

Either can work; what matters is agreeing on a team standard so you don’t debate it every time.


3. Queues are the default: avoid synchronous sending

Synchronous sending can stall requests due to SMTP/email API latency, causing timeouts and 500s. Email should generally be queued.

3.1 Queueing a Mailable

// app/Mail/WelcomeMail.php
class WelcomeMail extends Mailable implements \Illuminate\Contracts\Queue\ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public int $userId) {}

    public function build()
    {
        $user = User::findOrFail($this->userId);

        return $this->subject('Thanks for signing up')
            ->view('emails.welcome')
            ->text('emails.welcome_text')
            ->with(['user' => $user]);
    }
}

Sender side:

Mail::to($user->email)->queue(new WelcomeMail($user->id));

3.2 Make tries/backoff/timeout explicit

Because email delivery depends on external systems, retry policies are critical. If you wrap sending as a Job, it becomes easier to attach tries/backoff (covered later).


4. Prevent double sending: design idempotency in

Queues retry, so the same email can be sent twice. You want to prevent that by mechanism, not hope.

4.1 Persist a “sent” flag (the most robust)

For invoices and other critical messages, store send state in the DB.

Example: invoice mail

  • Add invoices.mail_sent_at
  • Check before sending, update after sending
if ($invoice->mail_sent_at) return;

Mail::to($invoice->user->email)->send(new InvoiceMail($invoice->id));
$invoice->forceFill(['mail_sent_at' => now()])->save();

4.2 Use a lock to prevent concurrent execution (as a helper)

$lock = cache()->lock("mail:invoice:{$invoice->id}", 120);
if (!$lock->get()) return;

try {
  // send (also combine with a sent-check)
} finally {
  $lock->release();
}

Notes:

  • A lock alone can leak via expiry or exceptions.
  • “Sent flag” + “lock” is the safest combo.

5. Handling failures: separate send errors, nondelivery, and bounces

“Your app handed it off successfully” and “it arrived to the recipient” are different. In operations, it helps to split:

  • Send failure: SMTP/API error; it never left your system
    • Handle via failed jobs, exception logs, retries
  • Nondelivery: it was sent, but spam filtering or recipient server issues prevented delivery
    • Domain authentication, sender reputation, content, list hygiene matter
  • Bounce: invalid address, rejection, etc.
    • Ingest bounce events and suppress those addresses (this is crucial)

On the Laravel side, start by making send failures highly visible:

  • Alerts for failed jobs
  • Send logs (mail type, target entity ID, recipient domain, trace_id)
    This alone speeds up incident response significantly.

6. Improve deliverability: minimum operations (SPF/DKIM/DMARC)

A large share of spam-folder issues comes down to sending-domain trust. Generally, these are foundational:

  • SPF: DNS declares which servers are authorized to send
  • DKIM: cryptographic signature proving the message wasn’t tampered with
  • DMARC: declares what to do with SPF/DKIM results (reject/quarantine/none)

This isn’t Laravel code—it’s DNS / mail-service configuration—but it’s the bedrock of deliverability, so it’s worth coordinating with operations.


7. Template governance: keep email stable even as it grows

7.1 Classify by intent

  • Transactional (required)
    • e.g., signup, reset, billing, invite, critical alerts
  • Semi-transactional (action-driving)
    • e.g., export complete, follow-up
  • Marketing (optional)
    • e.g., campaigns, weekly digests

Mixing these makes unsubscribe/opt-out and legal requirements harder. Even just separating “transactional vs everything else” is a big win.

7.2 Reuse shared layout

Standardize:

  • Header (service name)
  • Footer (support/contact, why the email was sent)
  • Button-like link styles (but plain links are fine)

Use a common layout and swap only the body; operations become far more stable.


8. Accessible email content: readability determines post-delivery satisfaction

HTML email has strict CSS limitations and varies wildly across clients. That’s exactly why structure is effective.

8.1 Subject line tips

  • Make the purpose instantly clear
  • Don’t overinclude personal info
  • If urgent, express urgency with words (don’t rely on symbols)

Examples:

  • “Invoice for February 2026”
  • “Password reset instructions (expires soon)”

8.2 Recommended body structure

  • Start: 1–2 sentences stating the purpose
  • Next: the action you want (bullets)
  • End: details (support, warnings, deadlines)

8.3 Make link text specific

Bad:

  • “Click here”
    Good:
  • “Reset your password”
  • “View your invoice”

This also works for screen-reader users who navigate via a list of links.

8.4 Don’t rely on color alone

Common failures: “the red button can’t be pressed,” “the color is too faint.”

  • Put important info in text
  • Use headings and lists instead of heavy bolding
  • Don’t depend on button-like visuals; prioritize link readability

8.5 Provide alt text for images

For decorative images like logos, use alt="" (empty) so they don’t clutter screen-reader output. Add alt only for meaningful images.


9. Implementation example: signup completion email (HTML + text)

9.1 HTML (example)

resources/views/emails/welcome.blade.php

<!doctype html>
<html lang="ja">
  <body>
    <h1>Thank you for signing up</h1>

    <p>{{ $user->name }}, your account has been created.</p>

    <h2>What you can do next</h2>
    <ul>
      <li>Set up your profile</li>
      <li>Create your first project</li>
    </ul>

    <p>
      You can log in from the following page:<br>
      <a href="{{ $loginUrl }}">Go to the login page</a>
    </p>

    <hr>

    <p>
      If you did not request this, please disregard this email.<br>
      If you need help, contact support.
    </p>
  </body>
</html>

9.2 Text (example)

resources/views/emails/welcome_text.blade.php

Thank you for signing up

{{ $user->name }}, your account has been created.

What you can do next
- Set up your profile
- Create your first project

Login page
{{ $loginUrl }}

If you did not request this, please disregard this email.

Notes:

  • Pairing HTML with a text version makes you resilient to client differences.
  • Keep sentences short and split paragraphs.
  • Use headings and bullets instead of overusing bold.

10. Tracking (opens/clicks): privacy cautions

Open/click tracking is useful, but it has privacy and policy implications:

  • Be explicit about what you measure
  • Define opt-out handling (especially for marketing email)
  • Be cautious about personally identifying tracking
  • Email clients may auto-load content, causing false positives

The “right” scope depends on your product, so decide a team policy before shipping.


11. Testing: use fakes to enforce behavior

Email tests should generally use Mail::fake().

use Illuminate\Support\Facades\Mail;

public function test_welcome_mail_is_queued()
{
    Mail::fake();

    $user = User::factory()->create();

    Mail::to($user->email)->queue(new \App\Mail\WelcomeMail($user->id));

    Mail::assertQueued(\App\Mail\WelcomeMail::class);
}

“Don’t double send” tests also become straightforward if you rely on a persisted sent flag.


12. Common pitfalls and how to avoid them

  • Slow pages / timeouts due to synchronous sending
    • Fix: queueing, timeout/backoff, monitoring
  • Double sending
    • Fix: sent flag + lock, idempotency
  • Landing in spam
    • Fix: SPF/DKIM/DMARC, consistent sender identity, healthy content
  • Broken HTML rendering
    • Fix: simple structure, include a text version
  • Unclear links
    • Fix: specific link text, short purpose statement up front
  • Tracking policy ambiguity
    • Fix: decide policy and disclosure (consent/opt-out)

13. Checklist (for distribution)

Delivery design

  • [ ] Separate transactional and marketing email
  • [ ] Default to queueing
  • [ ] Defined tries/backoff/timeout policy
  • [ ] Idempotency (sent-check) exists

Observability

  • [ ] Send logs (type, target ID, trace_id)
  • [ ] Alerts for failed jobs
  • [ ] Bounce ingestion and suppression policy

Deliverability

  • [ ] SPF/DKIM/DMARC configured
  • [ ] Consistent from/reply-to
  • [ ] Rules for sending rate and list management

Accessibility

  • [ ] Multipart HTML + text
  • [ ] Structured with headings and bullets
  • [ ] Link text is specific
  • [ ] Information is not color-dependent
  • [ ] Alt text is appropriate (decorative = empty)

Testing

  • [ ] Use Mail::fake()
  • [ ] Double-send prevention tests exist (for critical mail)

14. Conclusion

Laravel’s email stack is strong thanks to Mailable/Notification, and if you include operational design, it becomes impressively stable. Default to queueing; prevent double sends with retries + idempotency; make failures observable so you catch them early. Deliverability rests on SPF/DKIM/DMARC plus responsible list operations. And to ensure messages are understood after they arrive, standardize on accessible structure—headings, short paragraphs, bulleted lists, specific link text, and a plain-text version—so user experience improves while support load drops.


References

By greeden

Leave a Reply

Your email address will not be published. Required fields are marked *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)