To send Laravel mail through OwnMailServer, set MAIL_MAILER=smtp, MAIL_HOST=smtp.ownmailserver.com, MAIL_PORT=587, any MAIL_USERNAME and an API key as MAIL_PASSWORD in .env, with MAIL_FROM_ADDRESS on your verified domain. Leave MAIL_SCHEME empty so Laravel upgrades the connection with STARTTLS. Your mailables, notifications and password resets then go through us with no code changes.
This guide uses Laravel 11 to 13, which share the same mail configuration.
What you need before you start
- An approved OwnMailServer account with a verified sending domain. The From address must be on it, or we refuse the message with
550. The API docs list the DNS records, and the DKIM guide explains them. - An API key, created in your dashboard under API keys & SMTP. Choose Sending only (the default), limited to your sending domain: SMTP only needs to send. Use separate keys for staging and production.
Step 1: Set the mail settings in .env
MAIL_MAILER=smtp
MAIL_SCHEME=null
MAIL_HOST=smtp.ownmailserver.com
MAIL_PORT=587
MAIL_USERNAME=ownmailserver
MAIL_PASSWORD=mp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MAIL_FROM_ADDRESS="hello@acme.com"
MAIL_FROM_NAME="${APP_NAME}"
| Setting | Value | Why |
|---|---|---|
MAIL_SCHEME |
empty or null |
Port 587 starts plain and upgrades with STARTTLS. smtps is for port 465, which we don't offer. |
MAIL_USERNAME |
any value | We identify you by the API key. |
MAIL_PASSWORD |
an API key | The same key works for the API. |
MAIL_FROM_ADDRESS |
an address on your verified domain | The default for every mailable that doesn't set its own. |
Laravel 11 replaced the old MAIL_ENCRYPTION setting with MAIL_SCHEME. If you upgraded an older app, delete MAIL_ENCRYPTION=tls from .env, since it no longer does anything.
If your deploy runs php artisan config:cache, Laravel stops reading .env at runtime, so clear or rebuild the cache and restart the queue workers after changing these:
php artisan config:cache
php artisan queue:restart
Step 2: Send a test
php artisan tinker is the quickest check:
Mail::raw('It works.', fn ($m) => $m->to('you@example.com')->subject('Laravel test'));
No exception means we accepted it. Look the address up in your OwnMailServer dashboard to see the delivery. If you get an exception, the SMTP code in it is explained in the table at the end of this guide.
Step 3: Send real mail with a mailable
Mailables are Laravel's way of describing each email. Generate one with a Markdown template:
php artisan make:mail OrderShipped --markdown=mail.orders.shipped
// app/Mail/OrderShipped.php
namespace App\Mail;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OrderShipped extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public $tries = 5;
public $backoff = [10, 60, 300];
public function __construct(public Order $order) {}
public function envelope(): Envelope
{
return new Envelope(subject: "Your order #{$this->order->id} has shipped");
}
public function content(): Content
{
return new Content(markdown: 'mail.orders.shipped');
}
}
Mail::to($order->customer)->send(new OrderShipped($order));
Because the class implements ShouldQueue, send puts it on the queue instead of sending during the web request, so a slow SMTP connection never slows a page down. Run a worker with php artisan queue:work.
The $tries and $backoff properties matter here. Some of our refusals are temporary: 450 when you send faster than 10 messages a second or reach your daily warm-up cap. Laravel then throws an exception, and the queue retries the job after 10 seconds, a minute and five minutes instead of dropping the email.
Notifications sent through the mail channel and Laravel's built-in password reset and email verification messages use the same mailer, so they go through us too.
Step 4: Keep your old provider as a failover
A new account's daily cap starts at a few hundred recipients and grows about 25% after each clean day (how warm-up works). Laravel's failover mailer lets you switch now without waiting for that: it tries mailers in order and moves on when one throws.
In config/mail.php, add your old provider as a second mailer, then list both:
'mailers' => [
'smtp' => [ /* OwnMailServer, from .env as above */ ],
'old' => [
'transport' => 'smtp',
'host' => env('OLD_MAIL_HOST'),
'port' => env('OLD_MAIL_PORT', 587),
'username' => env('OLD_MAIL_USERNAME'),
'password' => env('OLD_MAIL_PASSWORD'),
],
'failover' => [
'transport' => 'failover',
'mailers' => ['smtp', 'old'],
'retry_after' => 60,
],
],
MAIL_MAILER=failover
Now an email we refuse, for example above your cap, goes out through the old provider instead. Watch your dashboard: once the cap is comfortably above your daily volume and the old provider has stopped picking up mail, set MAIL_MAILER=smtp and cancel it. Your old provider's domain records need to stay in place until then, so its mail still passes SPF and DKIM. Our SendGrid migration guide covers the DNS side.
Option: The HTTP API instead of SMTP
SMTP is the natural fit for Laravel, because every mailable, notification and package already uses it. For a one-off send where you want our response, such as the email id or an idempotency key, call the API with Laravel's HTTP client:
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.ownmailserver.key'))
->withHeaders(['Idempotency-Key' => "receipt-{$order->id}"])
->post('https://api.ownmailserver.com/emails', [
'from' => 'Acme <hello@acme.com>',
'to' => [$order->customer->email],
'subject' => "Receipt for order #{$order->id}",
'html' => view('mail.receipt', ['order' => $order])->render(),
])
->throw();
$emailId = $response->json('id');
Add 'ownmailserver' => ['key' => env('OMS_API_KEY')] to config/services.php. With the same Idempotency-Key within 24 hours, we return the first response and send nothing again, so a retried job can't email the customer twice.
If sending fails
| Exception mentions | Cause | Fix |
|---|---|---|
Connection could not be established / timed out |
Outbound port 587 is blocked | Allow outbound 587 on your server or host's firewall |
535 |
MAIL_PASSWORD isn't a valid API key |
Paste the full key; clear the config cache |
550 domain isn't verified |
The From address is on another domain | Set MAIL_FROM_ADDRESS, and any mailable's from, to your verified domain |
550 awaiting approval |
Your account is still being reviewed | Wait for approval, usually within one business day |
450 |
10 messages a second or the warm-up cap | Queued mail retries on its own; add failover for the cap |
452 |
More than 50 recipients | Send to fewer people per message |
552 |
Larger than 10 MB | Link to big files instead of attaching them |
After 5 failed logins from one IP address we block that address for 15 minutes, so fix a wrong password before retrying in a loop.
When this isn't the right fit
If your app sends a few thousand emails a month, any SMTP provider's free or entry plan will do, and Laravel makes switching between them a change of .env. We suit apps that want their own IP from 200,000 emails a month, warm-up and monitoring handled, and bounces and complaints suppressed automatically. Sending costs $0.20 per 1,000 emails, with a $10 monthly minimum that covers the first 50,000.