Step-by-step
-
1
Configure SMTP credentials in .env
Open your
.envfile and set theMAIL_*variables for your provider. For local development, setMAIL_MAILER=logto write emails tostorage/logs/laravel.loginstead of actually sending them. Switch tosmtpfor 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
Generate the Mailable class
Run
make:mailto scaffold the Mailable. The--markdownflag 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
Build the Mailable class
Open
app/Mail/WelcomeMail.php. Pass data via the constructor, then build the message in theenvelope()andcontent()methods. The$userwill be available as$userin 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
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--markdownflag, 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
Send the email from a controller
Import the
Mailfacade and callMail::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
Queue the email for production
Add
ShouldQueueto your Mailable and switch fromsend()toqueue()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
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.