Programming Beginner 9 min

How to Send Transactional Emails in Laravel

Laravel's Mail system wraps PHP's mailer into a clean, testable API. You define each email as a Mailable class — with its subject, template, and data — then send it in one line from anywhere in your application.

This guide covers configuring SMTP credentials, generating and building a Mailable, designing the Blade email template, and queueing emails so they don't slow down your HTTP response in production.

For local development, Mailtrap and Laravel's own log driver let you iterate without sending real emails.

Step-by-step

  1. 1

    Configure SMTP credentials in .env

    Open your .env file and set the MAIL_* variables for your provider. For local development, set MAIL_MAILER=log to write emails to storage/logs/laravel.log instead of actually sending them. Switch to smtp for staging and production.

    bash
    # .env — production (example with Mailgun SMTP)
    MAIL_MAILER=smtp
    MAIL_HOST=smtp.mailgun.org
    MAIL_PORT=587
    MAIL_USERNAME=postmaster@mg.yourdomain.com
    MAIL_PASSWORD=your-mailgun-smtp-password
    MAIL_ENCRYPTION=tls
    MAIL_FROM_ADDRESS=hello@yourdomain.com
    MAIL_FROM_NAME="${APP_NAME}"
    
    # .env — local development (no real emails sent)
    MAIL_MAILER=log
  2. 2

    Generate the Mailable class

    Run make:mail to scaffold the Mailable. The --markdown flag generates both the class and a Blade template stub using Laravel's built-in Markdown mail components. If you prefer plain HTML, omit the flag and create the view yourself.

    bash
    # With a Markdown template stub
    php artisan make:mail WelcomeMail --markdown=emails.welcome
    
    # With no template (you write the Blade view yourself)
    php artisan make:mail WelcomeMail
  3. 3

    Build the Mailable class

    Open app/Mail/WelcomeMail.php. Pass data via the constructor, then build the message in the envelope() and content() methods. The $user will be available as $user in the Blade template automatically because it's a public property.

    php
    <?php
    
    namespace App\Mail;
    
    use App\Models\User;
    use Illuminate\Bus\Queueable;
    use Illuminate\Mail\Mailable;
    use Illuminate\Mail\Mailables\Content;
    use Illuminate\Mail\Mailables\Envelope;
    use Illuminate\Queue\SerializesModels;
    
    class WelcomeMail extends Mailable
    {
        use Queueable, SerializesModels;
    
        public function __construct(
            public User $user
        ) {}
    
        public function envelope(): Envelope
        {
            return new Envelope(
                subject: 'Welcome to ' . config('app.name'),
            );
        }
    
        public function content(): Content
        {
            return new Content(
                view: 'emails.welcome',
            );
        }
    }
  4. 4

    Design the Blade email template

    Create resources/views/emails/welcome.blade.php. Keep the HTML simple — many email clients strip CSS. Inline styles are the safe bet; if you used the --markdown flag, the Markdown components handle styling for you.

    html
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
    </head>
    <body style="font-family: Arial, sans-serif; background: #f4f4f4; padding: 20px;">
        <div style="max-width: 600px; margin: 0 auto; background: #fff; padding: 30px; border-radius: 6px;">
            <h1 style="color: #333;">Welcome, {{ $user->name }}!</h1>
            <p style="color: #555; line-height: 1.6;">
                Your account has been created. You can now log in and get started.
            </p>
            <a href="{{ url('/login') }}"
               style="display: inline-block; background: #3b82f6; color: #fff;
                      padding: 10px 20px; border-radius: 4px; text-decoration: none;">
                Log In
            </a>
        </div>
    </body>
    </html>
  5. 5

    Send the email from a controller

    Import the Mail facade and call Mail::to($user)->send(new WelcomeMail($user)). This sends synchronously — the HTTP response waits for the mailer to finish. That's fine for local development but too slow for production (see the next step).

    php
    <?php
    
    namespace App\Http\Controllers;
    
    use App\Mail\WelcomeMail;
    use App\Models\User;
    use Illuminate\Support\Facades\Mail;
    
    class RegisterController extends Controller
    {
        public function store(Request $request)
        {
            $user = User::create($request->validated());
    
            Mail::to($user->email)->send(new WelcomeMail($user));
    
            return redirect('/dashboard');
        }
    }
  6. 6

    Queue the email for production

    Add ShouldQueue to your Mailable and switch from send() to queue() at the call site. The email is now dispatched to your queue driver (database, Redis, etc.) and delivered by a background worker, so your HTTP response stays fast.

    Make sure your queue worker is running: php artisan queue:work. On a production server, use Supervisor to keep it alive across restarts.

    php
    // app/Mail/WelcomeMail.php — add the interface
    use Illuminate\Contracts\Queue\ShouldQueue;
    
    class WelcomeMail extends Mailable implements ShouldQueue
    {
        use Queueable, SerializesModels;
        // ...
    }
    
    // In your controller — queue instead of send
    Mail::to($user->email)->queue(new WelcomeMail($user));
    
    // Or queue with a delay
    Mail::to($user->email)
        ->later(now()->addMinutes(5), new WelcomeMail($user));
  7. 7

    Use Mailtrap for development testing

    Mailtrap catches all outgoing mail in a sandbox inbox so you can inspect the rendered HTML, headers, and spam score without delivering to real addresses. Create a free account at mailtrap.io, then copy the SMTP credentials it provides into your local .env.

    bash
    # .env — Mailtrap
    MAIL_MAILER=smtp
    MAIL_HOST=smtp.mailtrap.io
    MAIL_PORT=2525
    MAIL_USERNAME=your_mailtrap_username
    MAIL_PASSWORD=your_mailtrap_password
    MAIL_ENCRYPTION=tls
    MAIL_FROM_ADDRESS=test@example.com
    MAIL_FROM_NAME="My App Dev"

Tips & gotchas

  • Always queue emails in production — sending synchronously blocks the request for however long your SMTP handshake takes, typically 200–800ms.
  • Use `Mail::fake()` in tests to assert that a specific Mailable was sent without actually connecting to an SMTP server.
  • Inline CSS manually or use a preprocessor like Premailer before sending — Gmail strips `<style>` blocks, breaking layouts that rely on them.
  • Set a reply-to address different from your from address when sending transactional email, so replies go to a monitored inbox rather than the sending service.
  • Test your email HTML in real clients (Gmail, Outlook, Apple Mail) using a service like Litmus or Email on Acid before launching — email rendering is notoriously inconsistent.

Wrapping up

You can now send and queue transactional emails in Laravel with a typed Mailable class, a Blade template, and a one-line dispatch call. The next step is to set up email events (like MessageSent) to log delivery attempts, or integrate a transactional email provider's webhook to track opens and bounces.

#Laravel #Email #SMTP
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.