brunocfalcao/laravel-zepto-mail-api-driver

Zeptomail api mail driver for Laravel

Installs: 133

Dependents: 1

Suggesters: 0

Security: 0

Stars: 0

Watchers: 1

Forks: 0

Open Issues: 0

pkg:composer/brunocfalcao/laravel-zepto-mail-api-driver

dev-master 2025-10-19 22:17 UTC

This package is auto-updated.

Last update: 2025-10-19 22:17:49 UTC


README

Latest Version on Packagist Total Downloads

A lightweight Symfony Mailer transport for Laravel that delivers mail via ZeptoMail’s HTTP API (no SMTP). It plugs into Laravel’s mailers and supports CC/BCC/Reply-To, attachments & inline (CID) images, single & batch sending, template sending (single & batch), custom MIME headers, open/click tracking flags, and a client reference. Under the hood it uses Laravel’s Http client, so you can Http::fake() in tests.

Requirements

  • PHP 8.1+
  • Laravel 9.x / 10.x / 11.x (Symfony Mailer)
  • Outbound HTTPS and TLS 1.2 available
  • A ZeptoMail account & API key (use the correct region endpoint, e.g. .com or .eu)

Installation

Install via Composer:

composer require brunocfalcao/laravel-zeptomail-driver

If you don’t use package discovery, register the provider manually:

// config/app.php
'providers' => [
    // ...
    Brunocfalcao\ZeptoMailApiDriver\ZeptoMailApiDriverServiceProvider::class,
],

Configuration

1) Environment

# .env
MAIL_MAILER=zeptomail
MAIL_FROM_ADDRESS=hello@yourdomain.com
MAIL_FROM_NAME="Your App"

# Driver secret
ZEPTOMAIL_MAIL_KEY=your-zeptomail-api-key

# Region endpoint (pick the one for your account):
ZEPTO_MAIL_ENDPOINT=https://api.zeptomail.com
# ZEPTO_MAIL_ENDPOINT=https://api.zeptomail.eu

# Optional tuning
ZEPTO_MAIL_TIMEOUT=30
ZEPTO_MAIL_RETRIES=2
ZEPTO_MAIL_RETRY_MS=200

# Optional defaults
ZEPTO_MAIL_TEMPLATE_KEY=           # or ZEPTO_MAIL_TEMPLATE_ALIAS=
ZEPTO_MAIL_BOUNCE_ADDRESS=         # templates API only
ZEPTO_MAIL_TRACK_OPENS=false
ZEPTO_MAIL_TRACK_CLICKS=false
ZEPTO_MAIL_CLIENT_REFERENCE=
ZEPTO_MAIL_FORCE_BATCH=false

The service provider reads config('services.zeptomail.mail_key') and falls back to env('ZEPTOMAIL_MAIL_KEY').

2) config/services.php

// config/services.php (excerpt)
return [
    // ...
    'zeptomail' => [
        // Primary: configure here. Fallback: env('ZEPTOMAIL_MAIL_KEY')
        'mail_key'        => env('ZEPTOMAIL_MAIL_KEY'),

        // Use .eu if your account lives in the EU region
        'endpoint'        => env('ZEPTO_MAIL_ENDPOINT', 'https://api.zeptomail.com'),

        // HTTP client tuning
        'timeout'         => env('ZEPTO_MAIL_TIMEOUT', 30),
        'retries'         => env('ZEPTO_MAIL_RETRIES', 2),
        'retry_sleep_ms'  => env('ZEPTO_MAIL_RETRY_MS', 200),

        // Optional defaults for convenience
        'template_key'    => env('ZEPTO_MAIL_TEMPLATE_KEY'),
        'template_alias'  => env('ZEPTO_MAIL_TEMPLATE_ALIAS'),
        'bounce_address'  => env('ZEPTO_MAIL_BOUNCE_ADDRESS'),
        'track_opens'     => env('ZEPTO_MAIL_TRACK_OPENS', false),
        'track_clicks'    => env('ZEPTO_MAIL_TRACK_CLICKS', false),
        'client_reference'=> env('ZEPTO_MAIL_CLIENT_REFERENCE'),
        'force_batch'     => env('ZEPTO_MAIL_FORCE_BATCH', false),
    ],
];

3) config/mail.php

// config/mail.php (excerpt)
return [
    'default' => env('MAIL_MAILER', 'smtp'),

    'mailers' => [
        'smtp' => [
            'transport' => 'smtp',
            // ...
        ],

        'zeptomail' => [
            'transport' => 'zeptomail', // registered by the ServiceProvider
        ],
    ],

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

Usage

You can use this transport like any other Laravel mailer.

Single email (default)

use Illuminate\Support\Facades\Mail;
use App\Mail\InvoiceReady;

Mail::to('a@example.com')->send(new InvoiceReady());
// -> POST /v1.1/email

Select the mailer per send

Mail::mailer('zeptomail')->to('a@example.com')->send(new InvoiceReady());

CC / BCC / Reply-To

Mail::mailer('zeptomail')->send(
    (new App\Mail\SimpleMessage)
        ->to('to@example.com')
        ->cc(['cc1@example.com', 'cc2@example.com'])
        ->bcc('audit@example.com')
        ->replyTo('reply@example.com')
);

Attachments & Inline (CID) images

// App\Mail\ReportMail
public function build()
{
    $cid = $this->embed(public_path('img/logo.png')); // <img src="cid:{{ $cid }}">
    return $this->subject('Monthly Report')
        ->view('emails.report', compact('cid'))
        ->attach(storage_path('app/reports/monthly.pdf')); // regular attachment
}

The driver maps attachments to attachments[] and CID images to inline_images[] with cid set accordingly.

Custom MIME headers, tracking, and client reference

public function build()
{
    return $this->subject('Security Alert')
        ->view('emails.security')
        ->withSymfonyMessage(function (Symfony\Component\Mime\Email $email) {
            // Custom headers → go under Zepto's mime_headers
            $email->getHeaders()->addTextHeader('X-Custom-Header', 'abc-123');

            // Tracking flags
            $email->getHeaders()->addTextHeader('X-Zepto-Track-Opens', 'true');
            $email->getHeaders()->addTextHeader('X-Zepto-Track-Clicks', 'false');

            // Optional client reference
            $email->getHeaders()->addTextHeader('X-Zepto-Client-Reference', 'user#42-event#login');
        });
}

Batch sending (per-recipient personalization & hidden recipients)

Use batch when sending to a collection of recipients. Recipients are not visible to each other. You can also provide per-recipient merge_info.

Batch (non-template)

use Illuminate\Support\Facades\Mail;

$recipients = ['alice@example.com', 'bob@example.com', 'carol@example.com'];

$perRecipient = [
    'alice@example.com' => ['name' => 'Alice', 'tier' => 'gold'],
    'bob@example.com'   => ['name' => 'Bob',   'tier' => 'silver'],
    // carol has no specific merge vars
];

Mail::to($recipients)->send(
    (new App\Mail\PromoMail)
        ->withSymfonyMessage(function (Symfony\Component\Mime\Email $email) use ($perRecipient) {
            // Force Zepto batch endpoint
            $email->getHeaders()->addTextHeader('X-Zepto-Batch', 'true');
            // Provide per-recipient merge vars
            $email->getHeaders()->addTextHeader('X-Zepto-PerRecipient-MergeInfo', json_encode($perRecipient));
        })
);
// -> POST /v1.1/email/batch

You can globally enforce batch via services.zeptomail.force_batch=true if you prefer.

Template sending

Send using a template key or alias (single or batch). Provide global merge_info, plus per-recipient merge_info in batch.

Single template email

use Illuminate\Support\Facades\Mail;

Mail::to('jane@example.com')->send(
    (new App\Mail\Templated)
        ->withSymfonyMessage(function (Symfony\Component\Mime\Email $email) {
            // Key or alias (the driver handles either)
            $email->getHeaders()->addTextHeader('X-Zepto-Template', 'my-template-alias'); // or 'ea36f19a...'

            // Global merge vars for this email
            $email->getHeaders()->addTextHeader('X-Zepto-MergeInfo', json_encode([
                'name'       => 'Jane',
                'reset_link' => 'https://example.com/reset/xyz',
            ]));

            // Optional bounce address (templates API)
            $email->getHeaders()->addTextHeader('X-Zepto-Bounce-Address', 'bounce@bounce.example.com');
        })
);
// -> POST /v1.1/email/template

Batch template email

use Illuminate\Support\Facades\Mail;

$recipients = ['a@example.com', 'b@example.com'];

$perRecipient = [
    'a@example.com' => ['name' => 'Alice', 'coupon' => 'ALC-10'],
    'b@example.com' => ['name' => 'Bob',   'coupon' => 'BOB-15'],
];

Mail::to($recipients)->send(
    (new App\Mail\Templated)
        ->withSymfonyMessage(function (Symfony\Component\Mime\Email $email) use ($perRecipient) {
            $email->getHeaders()->addTextHeader('X-Zepto-Template', 'my-template-alias');
            $email->getHeaders()->addTextHeader('X-Zepto-Batch', 'true');
            $email->getHeaders()->addTextHeader('X-Zepto-PerRecipient-MergeInfo', json_encode($perRecipient));

            // Optional global merge vars across all recipients
            $email->getHeaders()->addTextHeader('X-Zepto-MergeInfo', json_encode([
                'product' => 'Pro Plan',
            ]));
        })
);
// -> POST /v1.1/email/template/batch

Testing with Http::fake()

Because this driver uses Laravel’s HTTP client, you can fake the API easily:

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use App\Mail\InvoiceReady;

test('sends via zeptomail', function () {
    Http::fake([
        'https://api.zeptomail.com/*' => Http::response(['data' => [['message' => 'Email request received']]], 200),
        // or match your EU endpoint:
        // 'https://api.zeptomail.eu/*' => Http::response([...], 200),
    ]);

    Mail::to('test@example.com')->send(new InvoiceReady());

    Http::assertSent(function ($request) {
        return str_ends_with($request->url(), '/v1.1/email')
            && $request->method() === 'POST'
            && $request->json('subject') === 'Your invoice';
    });
});

Queues

Mailables work seamlessly on queues. Ensure your workers have the same .env values and outbound HTTPS access. If using Horizon, watch memory/timeouts on bursts of batch sends.

Troubleshooting

  • 401 / auth errors → Verify the key and that you’re using the correct regional endpoint (.com vs .eu).
  • “error” in response JSON → The driver throws if the Zepto response includes an error object; check domain/sender verification and payload shape.
  • Inline images not showing → Ensure you embed and reference the returned cid (<img src="cid:{{ $cid }}">).
  • Recipients visibility → Use batch endpoints to hide recipients; single-email endpoint can expose them.
  • Rate limits / large sends → Batch endpoints support a large number of recipients per request (subject to your Zepto plan). Split very large lists and back off with retries if needed.

How it works

  • ZeptoMailApiDriverServiceProvider registers the zeptomail transport.
  • ZeptoMailTransport converts the Symfony Email into Zepto payloads and calls the correct endpoint based on headers/config:
    • POST /v1.1/email (single)
    • POST /v1.1/email/batch (batch)
    • POST /v1.1/email/template (template single)
    • POST /v1.1/email/template/batch (template batch)

It also maps: from/to/cc/bcc/reply-to, subject, html/text, attachments, inline images, mime_headers, tracking flags, client_reference, and (for templates) merge_info & optional bounce_address.

Changelog

See CHANGELOG.

Contributing

PRs are welcome. If you add fields, please link the specific Zepto docs section in your PR description and include tests with Http::fake().

Security

Keep secrets in .env. Rotate API keys periodically. Consider setting a dedicated bounce address when using template APIs.

License

The MIT License (MIT). See LICENSE for details.