OquMail
Loading account…
Email API3 min readUpdated

Send Email from Laravel: SMTP Mailer or the HTTP API

Send email from Laravel on your own domain: point the smtp mailer at port 587 so Mailables just work, or call the API from a queued job. Env lines and pitfalls.

Laravel already knows how to send mail. Mailables, Notifications and the Mail facade all go through whatever mailer config/mail.php points at, so the quickest way to send from your own domain is to point the SMTP (the standard way computers hand off outgoing email) mailer at a mailbox on that domain. The HTTP API is the better fit when the send is part of a product flow and you want the delivery log and per-key limits. Here are both, with the .env lines, the queued job, and the four mistakes that produce silent failures.

Quick answer

For ordinary application mail, set MAIL_MAILER=SMTP (the standard way computers hand off outgoing email) with host mail.oqumail.com, port 587, the mailbox address as the username and MAIL_FROM_ADDRESS, and leave encryption as STARTTLS. Nothing in your Mailables changes. For the API, store the key in config/services.php, and POST from a queued job with Http::withToken(), sending to as an array.

Option 1: the smtp mailer

Create a mailbox for the application on your domain, for example app@yourdomain.com, and put its login in .env. Laravel 9 and 10 read MAIL_ENCRYPTION=tls as STARTTLS on port 587; Laravel 11 and 12 negotiate STARTTLS on 587 automatically, so the same values work there too. Either way the port is 587, never 465:

MAIL_MAILER=smtp
MAIL_HOST=mail.oqumail.com
MAIL_PORT=587
MAIL_USERNAME=app@yourdomain.com
MAIL_PASSWORD=the-mailbox-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=app@yourdomain.com
MAIL_FROM_NAME="${APP_NAME}"

Test it from tinker

Run php artisan config:clear first if the config is cached, then send a one-line test to a mailbox on your own domain:

php artisan tinker

>>> Mail::raw('Laravel SMTP test', fn ($m) => $m->to('you@yourdomain.com')->subject('Test from Laravel'));

Option 2: the API from a queued job

Add the key to .env as OQUMAIL_API_KEY and expose it through config/services.php as 'oqumail' => ['key' => env('OQUMAIL_API_KEY')]. The job below sends one message and records the id the API returns, so a support ticket can be matched to the delivery log:

<?php
// app/Jobs/SendTransactionalEmail.php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Http;

class SendTransactionalEmail implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;

    public function __construct(
        public string $to,
        public string $subject,
        public string $html,
        public string $text,
    ) {}

    public function handle(): void
    {
        $response = Http::withToken(config('services.oqumail.key'))
            ->timeout(15)
            ->post('https://api.oqumail.com/api/v1/emails', [
                'to' => [$this->to],
                'subject' => $this->subject,
                'html' => $this->html,
                'text' => $this->text,
                'fromName' => config('app.name'),
            ]);

        if (! $response->accepted()) { // the API answers 202 when the message is queued
            throw new \RuntimeException('send failed: '.$response->status().' '.$response->body());
        }

        logger()->info('email queued', ['to' => $this->to, 'id' => $response->json('id')]);
    }
}

Free business email on your own domain

OquMail gives you up to 15 mailboxes on your domain — free — with guided SPF/DKIM/DMARC, webmail, IMAP/SMTP for any mail app, and a send API. Setup is a few DNS records; changes usually take effect within minutes, occasionally up to 48 hours. Start at oqumail.com.

Get started free

Dispatching it

From a controller or an event listener: SendTransactionalEmail::dispatch($user->email, 'Welcome', $html, $text). With QUEUE_CONNECTION=sync in local development the job runs inline; in production run a queue worker, or the job never executes and nobody is told.

Which option when

  • Mailables, Notifications, password resets and verification mail: the SMTP (the standard way computers hand off outgoing email) mailer. Zero code changes and every Laravel feature keeps working.
  • Sends that belong to a product flow (receipts, OTPs, alerts) where you want the API delivery log, a revocable key per integration and the per-key limits: the API job.
  • Both at once is fine: the SMTP (the standard way computers hand off outgoing email) mailer for framework mail and the API for high-value transactional sends.

Pitfalls specific to Laravel

  • MAIL_ENCRYPTION=ssl or port 465. The relay speaks STARTTLS on 587; the wrong pair fails with a TLS or connection error.
  • MAIL_FROM_ADDRESS that is not the mailbox you log in as. The relay only sends as the authenticated mailbox, so a mismatched From is rejected or rewritten.
  • A cached config. After editing .env run php artisan config:clear, or the old mailer settings stay in use.
  • Queued mail with no worker. QUEUE_CONNECTION=database or redis in production needs php artisan queue:work running under a supervisor.
  • Passing to as a string to the API. It must be an array even for one recipient, or the API returns 400.
  • Logging the API key. Never put the key in a log context or an exception message.

Common questions

Do Notifications work with the smtp mailer?

Yes. The mail channel uses the default mailer, so once .env points at your mailbox every Notification goes out from your domain. Set the from address in config/mail.php or per notification.

How do I attach a file?

With the SMTP (the standard way computers hand off outgoing email) mailer use the Mailable attach() method as usual. With the API, add an attachments array of objects with filename, contentType and contentBase64 (base64_encode the file contents) to the request body.

Does this work on Forge, Vapor or Docker?

Yes. Set the same environment variables in the hosting panel or the container environment. On Vapor and other serverless hosts prefer the API option, since outbound SMTP from function runtimes is often blocked or slow.