duva-mail / laravel
Laravel mail transport for Duva, the transactional email API hosted in Canada.
Requires
- php: >=8.2
- duva-mail/duva: ^0.2
- illuminate/mail: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- symfony/mailer: ^7.0 || ^8.0
- symfony/mime: ^7.0 || ^8.0
Requires (Dev)
- orchestra/testbench: ^10.0 || ^11.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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*andX-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 originalDuva\Exceptions\*exception is itsgetPrevious()), so Laravel's fail-over and your owncatchblocks 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.