Application email QA

Test welcome email delivery in Laravel: assertions and cleanup

Assert Laravel welcome mail generation in automated tests, then manually confirm SMTP handoff with a receive-only inbox—without treating Mailby as a CI API.

Welcome envelope on a test bench with success and failure stamps

For welcome email delivery in Laravel, assert application generation with Mail::fake() (or notification fakes) in PHPUnit/Pest, then separately verify real SMTP handoff to a mailbox you control. Use a receive-only temporary inbox for manual inbound checks. Do not assume a Mailby CI API is required for the unit layer—and when you use the live developer tools, treat them as the product’s documented API/console, not as a substitute for Laravel’s mail fakes.

This guide separates four layers so failures stay diagnosable.

Laravel context and boundaries

Welcome mail usually means: user registers → app queues/sends a WelcomeMail mailable or notification → SMTP/API provider accepts it → user inbox renders it.

Layers:

  1. Application generation — correct recipient, subject, view data
  2. SMTP / API handoff — credentials, queues, rate limits
  3. Inbound delivery — provider → recipient MX
  4. User experience — HTML/text rendering, CTA links

Automated Laravel tests shine at layer 1. Layers 2–4 need staging sends or manual receive checks.

Product truth: Mailby Quick Inbox is live at /inbox for receive-only capture. Developer/Test Inbox Cloud API and console are live at /developers and /account/developer for programmatic receive workflows when that fits your stack. Laravel’s first-line unit tests should still use framework fakes.

Minimal authorized test (layer 1)

use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Support\Facades\Mail;

public function test_welcome_mail_is_generated_for_new_user(): void
{
    Mail::fake();

    $user = User::factory()->create([
        'email' => 'new-user@example.test',
    ]);

    // Trigger the same path registration uses (event, action, or job)
    $this->post('/register', [
        'name' => 'Ada',
        'email' => $user->email,
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    Mail::assertSent(WelcomeMail::class, function (WelcomeMail $mail) use ($user) {
        return $mail->hasTo($user->email)
            && $mail->assertHasSubject('Welcome'); // adjust to your API
    });
}

Adapt to your Laravel version’s assertion helpers (assertQueued if you queue mailables). The point is: no real network in the default test suite.

Negative cases

  • Registration validation fails → Mail::assertNothingSent()
  • Duplicate email → no second welcome
  • User opts out of marketing but still receives transactional welcome if that is your policy—assert the distinction explicitly

Field evidence: working path (manual receive)

On 2026-09-24, against a local Laravel app with Mailpit/log driver swapped to a real SMTP staging account:

  1. Registered a user whose address was a Mailby Quick Inbox receive-only address.
  2. Confirmed the mailable fired in logs.
  3. Opened /inbox session, received the welcome HTML.
  4. Checked that the CTA pointed at the staging app host (not production).

Working path: End-to-end receive confirmed without polluting a personal mailbox.

Failure / limitation

When QUEUE_CONNECTION=database and the queue worker was stopped, Laravel “sent” nothing to SMTP. Unit tests with Mail::fake() still passed. Lesson: green unit tests do not prove delivery. Always keep a smoke path that observes a real message.

Test case table

Test caseExpected messageObservable evidenceNegative case
Happy registrationWelcome mailable to userMail::assertSent / inbox receiveValidation error → nothing sent
Queued welcomeJob pending then sentassertQueued + worker processesWorker down → no SMTP
Wrong env mailerStaging host in linksHTML contains staging URLProduction URL leak
Localized welcomeCorrect locale templateSubject/body languageFallback locale unexpectedly
Soft-deleted userNo welcomeassertNothingSentMail still generated

Worked example: cleanup

Temporary addresses and staging users accumulate.

  1. Delete staging users tagged qa-welcome-*.
  2. Flush Redis/database queues between suites.
  3. Rotate SMTP sandbox credentials in CI secrets—not in git.
  4. Close Quick Inbox sessions when finished; do not screenshot live addresses into public tickets.

For browser-driven identity isolation patterns, see isolating test identities in Selenium.

SMTP handoff checklist (layer 2)

  • MAIL_MAILER, host, port, encryption match provider docs (Laravel mail docs)
  • From address aligns with SPF/DKIM domains
  • Queue retries have backoff; poison messages are visible
  • Failures land in logs/Sentry, not silent report() swallows

When not to use a temporary inbox

  • Testing password reset for production users
  • Capturing real customer PII
  • Any flow that requires the test to send mail from the temporary address (Mailby will not)

Prefer durable shared QA mailboxes for long-lived staging seed accounts; use temporary receive-only for one-shot delivery proofs.

Short answers

What causes welcome email delivery bugs in Laravel?
Misconfigured mailers, stopped queues, wrong env, or asserting the wrong layer.

What should I do first?
Mail::fake() assertions on the registration path, then one staging SMTP smoke receive.

When is a permanent address safer?
Shared staging identities that must receive mail for weeks.

What evidence changes the recommendation?
You need automated inbound assertions via API—then evaluate /developers against your CI design rather than scraping a browser inbox.

Sources, test date, limitations

  • Narrative date: 2026-09-24.
  • External: Laravel Mail documentation.
  • Examples are illustrative; adjust to your Laravel major version.
  • Mailby does not send/forward; receive-only observation only.

Expanding the Laravel test pyramid for mail

Feature tests vs unit tests

Unit-level: assert a listener or action builds WelcomeMail with the right user. Feature-level: post to /register and assert sent/queued. HTTP-level smoke: optional staging send to a receive-only inbox.

Keep network I/O out of the default PR gate. Run smoke on a schedule or pre-release job.

Notifications vs Mailables

Laravel apps often use Notification channels. Fake with Notification::fake() and assert MailMessage contents. Do not mix Mail::fake() expectations if the code path only notifies.

Queues, Horizon, and failed jobs

Welcome mail delayed by a queue looks like “delivery broken” in Selenium even when generation tests pass. Monitor failed_jobs. Add a test that WelcomeMail is queued when that is the design.

Localization and themes

If you brand welcome mail per tenant, assert view data includes tenant.name and that the HTML does not leak another tenant’s logo path—common multi-tenant bug.

Link correctness

Parse the CTA URL in tests:

Mail::assertSent(WelcomeMail::class, function ($mail) {
    $html = $mail->render();
    return str_contains($html, config('app.url'));
});

Prevent production URLs in staging and vice versa.

Manual receive protocol

  1. Set staging SMTP to a sandbox provider or relay you control.
  2. Register with a Mailby address from /inbox or an API identity from /developers.
  3. Confirm subject, From alignment, and CTA host.
  4. Record the message-id in the QA ticket; then let the temporary inbox expire.

Cleanup automation ideas

  • Nightly job deletes users with email like 'qa-%'
  • CI uses unique emails per run (UUID)
  • Never commit captured message bodies containing real customer data

How this differs from the test-email-workflows hub

Hub pages survey strategies. This article is Laravel-specific: fakes, queues, and a receive-only confirmation path with explicit product boundaries for Mailby.

Example Pest test sketch

it('sends welcome mail after registration', function () {
    Mail::fake();
    $response = $this->post('/register', validRegistrationPayload());
    $response->assertRedirect();
    Mail::assertSent(WelcomeMail::class);
});

it('does not send welcome mail when registration fails', function () {
    Mail::fake();
    $this->post('/register', invalidRegistrationPayload());
    Mail::assertNothingSent();
});

Keep payloads in factories. Assert nothing sent on failure—this catches accidental mail in validation error paths.

Staging smoke checklist (copy/paste)

  • APP_URL is staging
  • Mail From domain authorized
  • Queue worker running
  • Register QA user with unique email
  • Message visible in receive-only inbox
  • CTA host is staging
  • Delete QA user

Incident: “Users say welcome never arrived”

Triage order: app logs → queue → ESP dashboard → recipient junk → MIME content. Do not start by rewriting templates. Measure each hop.

Environment matrix

EnvironmentMailerAssert how
phpunitarray / fakeMail::fake
locallog or MailpitInspect UI
stagingreal SMTP sandboxReceive-only inbox
productionproviderMetrics only, no QA probes to prod customers

Never point automated tests at production customer mail flows.

Idempotency of welcome sends

If users refresh registration confirmation, do they get three welcomes? Assert once-per-user with a welcome_sent_at column or equivalent. Duplicate welcomes annoy and hurt ESP reputation.

Content security

Welcome mails that include unsigned temporary login tokens in URLs should expire quickly and land only on HTTPS. Assert token TTL in unit tests where possible.

Observability

Log message_id, user_id, and mailer driver on send. When someone files “welcome missing,” correlate without reading message bodies in shared slack.

Pairing with browser tests

Selenium can complete registration and wait on a mail harness (identity isolation). Keep Laravel fakes for speed; keep one coupled path for confidence.

Release gate proposal

Before promoting a build that touches registration:

  1. Unit/feature mail fakes green in CI
  2. Staging smoke receive succeeds once
  3. No failed jobs for WelcomeMail in the last hour of soak
  4. Template screenshots attached to the PR for HTML changes

Skipping (2) is the most common cause of “but tests passed” incidents. Mailby Quick Inbox or developers can serve as the receive side for (2) without polluting personal mail.

Common Laravel gotchas checklist

  • Using Mail::send in a constructor
  • Forgetting ShouldQueue while assuming async
  • Rendering markdown mail with undefined $user variables
  • Hard-coding From that fails SPF
  • Testing only happy path registration

Walk this list during code review on any PR touching welcome mail.

Walkthrough: debugging a missing welcome in staging

  1. Reproduce registration with a unique QA email.
  2. Confirm users row exists.
  3. Check jobs / Horizon for pending mail.
  4. Check failed_jobs for exceptions rendering the mailable.
  5. Check ESP acceptance logs for 250 vs 550.
  6. Check the receive-only inbox and staging user’s personal junk.
  7. Only then change template code.

Most “template bugs” die at step 3 or 4. Keep this order on an index card near the on-call laptop. When using Mailby as the receive target, note the session expiry so step 6 happens inside the lease.

Documentation debt

Update your internal README with the mailer env vars and the smoke receive procedure. Future teammates should not invent a new path that sends welcome mail to production customers by accident.

Treat welcome mail as a production dependency equal to payments: owned, monitored, and tested on every release train that touches registration. Shortcuts here become customer-trust incidents tomorrow.

Ownership note

Assign a named engineer as mailer owner for each app. Orphaned SMTP configs are how staging silently points at the wrong provider for months. The owner reviews welcome-mail PRs and keeps the smoke-receive checklist current when Mailby or the ESP workflows change.

Conclusion

Prove welcome email delivery in Laravel in layers: fake the generator in CI, smoke-test SMTP to a mailbox you control, and clean up identities afterward. Quick Inbox is a practical manual receive target; the developers surface supports programmatic receive when you design for it—without replacing framework mail fakes.

For inbound DNS issues after you deploy the receiving side of your own stack, see DNS cache after migration runbook.

Try it on Mailby

Open a receive-only disposable inbox when a short-lived address fits the job — session-bound, with timed purge.