Search by

duva-mail / laravel

vgibara

Laravel mail transport for Duva, the transactional email API hosted in Canada.

v0.1.0 2026-10-09 17:07 UTC

This package is auto-updated.

Last update: 2026-10-09 17:16:56 UTC


README

Laravel mail transport for Duva, the transactional email API hosted in Canada. Use Mail, Mailables and Notifications as usual; the messages go out through Duva.

Requires PHP 8.2+ and Laravel 12 or 13 (Laravel 11 no longer receives security fixes). Built on duva-mail/duva.

Install

composer require duva-mail/laravel

The package registers itself (Laravel package discovery). You also need a Duva API key and a verified domain (documentation).

Configure

.env:

MAIL_MAILER=duva
DUVA_API_KEY=dv_...
DUVA_DOMAIN=example.com
MAIL_FROM_ADDRESS=notifications@example.com
MAIL_FROM_NAME="Example"

config/mail.php, in mailers:

'duva' => [
    'transport' => 'duva',
    'key' => env('DUVA_API_KEY'),
    'domain' => env('DUVA_DOMAIN'),
],

Optional keys: base_url (default https://api.duva.ca), timeout (seconds, default 10), max_retries (default 2). The From address must belong to the configured domain.

Use it in a fail-over chain like any other transport:

'failover' => ['transport' => 'failover', 'mailers' => ['duva', 'smtp']],

What is sent

Laravel / Symfony Duva
From, To, Cc, Bcc, Reply-To from, to, cc, bcc, reply_to (a Bcc only in the envelope is sent as Bcc)
HTML and text bodies html, text
Attachments, inline images (embed) attachments (inline ones keep their content_id)
Mailable::tag() tags
Mailable::metadata() metadata
List-Unsubscribe, List-Unsubscribe-Post, List-Id, In-Reply-To, References, Auto-Submitted, Precedence, Importance, Feedback-ID, and X-* headers headers

Notes:

  • Other headers are not forwarded (Duva rejects them), nor are X-Duva*, X-Kumo*, X-Tenant* and X-Campaign*.
  • A display name Duva refuses (it contains @, quotes, <>, control characters or an encoded word =?…?=) is dropped; the address is kept.
  • The message ID Laravel reports is Duva's.
  • Delivery is asynchronous: a successful send() means Duva accepted the message, not that it was delivered. Use Duva's webhooks or events for the outcome.
  • A Duva error is thrown as Symfony\Component\Mailer\Exception\TransportException (the original Duva\Exceptions\* exception is its getPrevious()), so Laravel's fail-over and your own catch blocks behave as with any transport.

Idempotency

A retried queue job renders a new message, so it would be sent twice. Set a stable key for it (an order ID, for example):

public function build()
{
    return $this->withSymfonyMessage(function ($message) {
        $message->getHeaders()->addTextHeader('X-Idempotency-Key', "order-{$this->order->id}");
    });
}

The header is read by the transport and never sent. Duva ignores a second message with the same key.

Testing your application

Use Laravel's Mail::fake() as usual. To exercise the transport itself without a network, bind a fake transport of the client library:

use Duva\Http\TestTransport;

$http = new TestTransport(fn () => TestTransport::response(['id' => 'msg_1', 'status' => 'queued']));
app()->instance('duva.transport', $http);
// ... send mail ...
$http->requests[0]->body; // what would have been sent

License

MIT.