Reader Stacks

Sending Email in Laravel: SMTP, Gmail, SendGrid, and Attachments

Every Laravel mail-sending scenario — a plain SMTP relay, Gmail specifically, SendGrid, an attachment, multiple recipients — goes through the exact same Mailable class, differing only in .env configuration and the Mailable's own build logic.

Every one of Laravel's mail-sending scenarios — a generic SMTP server, Gmail specifically, a transactional service like SendGrid, sending to multiple recipients, or attaching a file — goes through the same Mailable class structure, differing mainly in .env configuration.

Creating a Mailable

php artisan make:mail OrderConfirmation
class OrderConfirmation extends Mailable
{
    public function __construct(public Order $order) {}

    public function build()
    {
        return $this->subject('Your Order Confirmation')
            ->view('emails.order-confirmation')
            ->with(['order' => $this->order]);
    }
}

Sending it

Mail::to($order->customer_email)->send(new OrderConfirmation($order));

Generic SMTP configuration

MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tls

Gmail SMTP configuration specifically

MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
MAIL_ENCRYPTION=tls

Gmail specifically requires an "app password" rather than the actual account password once 2-factor authentication is enabled (which Google effectively requires for this use case) — generated separately in the Google Account security settings, not the regular login password.

SendGrid configuration (via SMTP relay)

MAIL_MAILER=smtp
MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USERNAME=apikey
MAIL_PASSWORD=your-sendgrid-api-key
MAIL_ENCRYPTION=tls

The literal string apikey as the username is correct and intentional for SendGrid's SMTP relay — this isn't a placeholder to replace, it's the actual expected value, with the real SendGrid API key going in the password field instead.

Sending to multiple recipients, with CC

Mail::to($primaryEmail)
    ->cc(['manager@example.com', 'accounting@example.com'])
    ->send(new OrderConfirmation($order));

// Sending the same email to many recipients individually (not all in one To: header)
foreach ($subscribers as $subscriber) {
    Mail::to($subscriber->email)->send(new NewsletterMail($content));
}

Looping and calling Mail::to() individually per recipient (rather than passing an array to a single to() call) keeps each recipient's email address private from the others — passing multiple addresses to one to() call puts them all in the same visible To: header.

Attaching a file

public function build()
{
    return $this->subject('Your Invoice')
        ->view('emails.invoice')
        ->attach(storage_path('app/invoices/invoice-1001.pdf'), [
            'as' => 'invoice.pdf',
            'mime' => 'application/pdf',
        ]);
}

Queueing emails instead of sending synchronously

class OrderConfirmation extends Mailable implements ShouldQueue
{
    // ...
}

Implementing ShouldQueue on the Mailable defers the actual sending to a background queue worker rather than blocking the current request — worth doing for any email sent as part of a user-facing request (like this order confirmation), so a slow or temporarily unreachable mail server doesn't delay the response the user is actively waiting for.