0

Mail drivers — SMTP, Mailgun, SES, log, array

Beginner5 min read·lv-22-002

Concept

Laravel's mail system is driver-based: a single MAIL_MAILER environment variable switches the transport without changing any application code. Under the hood, all drivers are wrapped by Illuminate\Mail\Mailer, which delegates to a Symfony\Component\Mailer\Transport\TransportInterface implementation. Since Laravel 9 dropped SwiftMailer in favor of Symfony Mailer, the transport layer gained first-class support for DSN-based configuration, async sending (when used with Symfony's async transport), and better error reporting.

SMTP is the baseline. Set MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, and MAIL_ENCRYPTION in your .env. TLS on port 587 is the modern standard; STARTTLS negotiation is handled automatically. SMTP works with any provider — Gmail (with app passwords), your own Postfix, Mailpit in local dev, or a hosted relay like SendGrid via SMTP credentials.

Mailgun and SES are HTTP-based transports. Instead of opening an SMTP connection, Laravel calls the provider's API via HTTP. This is faster (no SMTP handshake), more reliable under high volume, and gives access to provider-level delivery events. Mailgun requires symfony/mailgun-mailer and symfony/http-client packages; SES requires league/flysystem-aws-s3-v3 — actually symfony/amazon-mailer and AWS credentials. Always check composer require instructions in the Laravel docs for the current package names, as they change between major versions.

log and array drivers are for development and testing. The log driver writes the raw email content to your Laravel log file — useful for local dev without a real SMTP server. The array driver stores sent mails in memory, which is what Mail::fake() builds on internally. Never configure log or array in production.

You can define multiple mailers in config/mail.php under the mailers key and send through a specific one with Mail::mailer('ses')->send(...). This is the right pattern when your app sends transactional mail through SES but marketing mail through Mailgun, for example.

DriverTransportPackage NeededBest For
smtpSMTP socketnoneGeneral purpose, any provider
mailgunHTTP APIsymfony/mailgun-mailerHigh-volume transactional
sesAWS APIsymfony/amazon-mailerAWS-native stacks
postmarkHTTP APIsymfony/postmark-mailerDeliverability focus
logFilesystemnoneLocal development
arrayMemorynoneTesting

Code Example

php
// config/mail.php — multiple mailer configuration
return [
    'default' => env('MAIL_MAILER', 'smtp'),

    'mailers' => [
        'smtp' => [
            'transport' => 'smtp',
            'host'      => env('MAIL_HOST', 'mailpit'),
            'port'      => env('MAIL_PORT', 1025),
            'encryption' => env('MAIL_ENCRYPTION', null),
            'username'  => env('MAIL_USERNAME'),
            'password'  => env('MAIL_PASSWORD'),
            'timeout'   => null,
        ],

        'mailgun' => [
            'transport' => 'mailgun',
            // Mailgun config pulled from services.php
        ],

        'ses' => [
            'transport' => 'ses',
            // AWS credentials from services.php
        ],

        'log' => [
            'transport' => 'log',
            'channel'   => env('MAIL_LOG_CHANNEL'),
        ],

        'array' => [
            'transport' => 'array',
        ],
    ],

    'from' => [
        'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
        'name'    => env('MAIL_FROM_NAME', 'Example App'),
    ],
];

// config/services.php — Mailgun credentials
'mailgun' => [
    'domain'   => env('MAILGUN_DOMAIN'),
    'secret'   => env('MAILGUN_SECRET'),
    'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
    'scheme'   => 'https',
],

// Sending via a specific mailer
Mail::mailer('ses')
    ->to($user->email)
    ->send(new OrderShipped($order));

// Or in a Mailable class itself
public function mailer(): string
{
    return 'mailgun';
}

Interview Q&A

Q: What replaced SwiftMailer in Laravel 9 and what concrete benefit did that change bring?

Laravel 9 replaced SwiftMailer with Symfony Mailer. The practical benefits include DSN-based transport configuration (a single connection string instead of many discrete config keys), better native support for cloud provider API transports (Mailgun, SES, Postmark) as first-class Symfony packages, improved error messages and exception types from the transport layer, and alignment with a maintained upstream library — SwiftMailer was effectively abandoned. The public Laravel API (Mail::send(), Mailable classes) did not change, so most applications required no code changes for the upgrade.


Q: When would you choose an HTTP API transport like Mailgun over plain SMTP even though SMTP "also works" with Mailgun?

HTTP API transports are faster because they skip the SMTP handshake (no TCP connection negotiation, no EHLO/AUTH round trips), they provide structured API responses with delivery IDs you can use for webhook correlation, they handle connection pooling and retries at the provider level rather than in your app, and they expose provider-specific features like message tagging, scheduled delivery, and template variables that aren't available over generic SMTP. For high-volume or latency-sensitive sending, the API transport is always preferred. SMTP is fine for low volume or when you're behind a mail relay you don't control.


Q: How do you safely test outgoing mail locally without sending real emails?

The cleanest local setup is Mailpit (or the older MailHog) — a local SMTP server that catches all outgoing mail and displays it in a web UI. Configure MAIL_HOST=localhost, MAIL_PORT=1025, MAIL_ENCRYPTION=null and all mail is caught without reaching real recipients. For automated tests, use Mail::fake() which swaps the mailer with the array driver and provides assertion helpers. Never rely on the log driver as your only local mail strategy — it gives you raw MIME output in a log file, which is hard to read and doesn't render HTML.