PHPackages                             julienramel/cloudflare-mailer - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. julienramel/cloudflare-mailer

ActiveSymfony-mailer-bridge[Mail &amp; Notifications](/categories/mail)

julienramel/cloudflare-mailer
=============================

Symfony Mailer bridge for Cloudflare Email Service

v1.0.0(3w ago)026↓50%MITPHPPHP &gt;=8.2CI passing

Since May 14Pushed 3w agoCompare

[ Source](https://github.com/JulienRamel/cloudflare-mailer)[ Packagist](https://packagist.org/packages/julienramel/cloudflare-mailer)[ Docs](https://github.com/julienramel/cloudflare-mailer)[ RSS](/packages/julienramel-cloudflare-mailer/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (7)Versions (2)Used By (0)

Cloudflare Mailer
=================

[](#cloudflare-mailer)

[![Tests](https://github.com/julienramel/cloudflare-mailer/actions/workflows/tests.yml/badge.svg)](https://github.com/julienramel/cloudflare-mailer/actions/workflows/tests.yml)[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](LICENSE)

Symfony Mailer bridge for [Cloudflare Email Service](https://developers.cloudflare.com/email-service/).

> **Note:** Cloudflare Email Sending is currently in **public beta**. The API may evolve before general availability. Pin your dependency to a specific version of this package.

Requirements
------------

[](#requirements)

- PHP 8.2+
- Symfony Mailer 6.4+
- A Cloudflare account with [Email Sending configured](https://developers.cloudflare.com/email-service/get-started/send-emails/)

Installation
------------

[](#installation)

```
composer require julienramel/cloudflare-mailer
```

Configuration
-------------

[](#configuration)

### DSN

[](#dsn)

```
MAILER_DSN=cloudflare+api://ACCOUNT_ID:API_TOKEN@default

```

PartDescription`ACCOUNT_ID`Your [Cloudflare Account ID](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/)`API_TOKEN`A Cloudflare API token with **Email Sending** permission### Symfony Mailer (`config/packages/mailer.yaml`)

[](#symfony-mailer-configpackagesmaileryaml)

```
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'
```

### Register the transport factory (`config/services.yaml`)

[](#register-the-transport-factory-configservicesyaml)

```
services:
    JulienRamel\CloudflareMailer\Transport\CloudflareTransportFactory:
        tags:
            - { name: mailer.transport_factory }
```

Usage
-----

[](#usage)

```
use Symfony\Component\Mime\Email;
use Symfony\Component\Mailer\MailerInterface;

class MyService
{
    public function __construct(private readonly MailerInterface $mailer) {}

    public function sendWelcome(string $to): void
    {
        $email = (new Email())
            ->from(new Address('noreply@yourdomain.com', 'My App'))
            ->to($to)
            ->subject('Welcome!')
            ->text('Thanks for signing up.')
            ->html('Thanks for signing up.');

        $this->mailer->send($email);
    }
}
```

Handling bounces
----------------

[](#handling-bounces)

Unlike most email providers (which report bounces asynchronously via webhooks), Cloudflare includes permanent bounce information **directly in the API response**. This bridge surfaces it via a Symfony event so your application can react immediately.

### How it works

[](#how-it-works)

```
send() called
    │
    ├── HTTP error / API failure  →  HttpTransportException thrown
    │
    └── HTTP 200 + success: true
            │
            ├── no permanent bounces  →  SentMessage returned, nothing else
            │
            └── permanent bounces present  →  CloudflareBounceEvent dispatched
                                               SentMessage returned (always)

```

A bounce — even a total one — is a **business failure, not a transport failure**. The API call itself succeeded. No exception is thrown; your listener decides what to do.

### Registering the listener

[](#registering-the-listener)

```
use JulienRamel\CloudflareMailer\Event\CloudflareBounceEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
final class CloudflareBounceListener
{
    public function __construct(
        private readonly ContactRepository $contacts,
    ) {}

    public function __invoke(CloudflareBounceEvent $event): void
    {
        foreach ($event->getBouncedAddresses() as $address) {
            // The address does not exist or is permanently unreachable.
            // Common reactions: mark as invalid, remove from mailing list,
            // alert your ops team, increment a counter, etc.
            $this->contacts->markAsUndeliverable($address);
        }
    }
}
```

### Treating a total bounce as a fatal error

[](#treating-a-total-bounce-as-a-fatal-error)

If your use case requires an exception when nobody received the email, throw it yourself inside the listener:

```
use JulienRamel\CloudflareMailer\Event\CloudflareBounceEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Mailer\Exception\TransportException;

#[AsEventListener]
final class StrictBounceListener
{
    public function __invoke(CloudflareBounceEvent $event): void
    {
        $result = $event->getSentMessage()->getDebug();
        // Check if anything was delivered by inspecting the debug output,
        // or keep track of delivered/bounced counts in your own logic.

        // Throw to propagate the failure up the call stack:
        throw new TransportException(\sprintf(
            'Email permanently bounced for: %s',
            implode(', ', $event->getBouncedAddresses()),
        ));
    }
}
```

### Inspecting results without a listener

[](#inspecting-results-without-a-listener)

`SentMessage::getDebug()` always contains a human-readable summary visible in the Symfony web profiler:

```
Cloudflare Email result:
  Delivered: alice@example.com
  Queued:    none
  Permanent bounces: ghost@nonexistent.tld

```

Supported features
------------------

[](#supported-features)

FeatureSupportedPlain text body✅HTML body✅CC / BCC✅Reply-To✅Attachments✅Inline images (`cid:`)✅Custom headers✅Bounce detection✅ (synchronous, via event)SMTP❌ (API only)Known limitations
-----------------

[](#known-limitations)

**Recipient display names are not supported.** The Cloudflare REST API only accepts plain email addresses for `to`, `cc`, and `bcc` fields. If you set `new Address('john@example.com', 'John Doe')` as a recipient, only `john@example.com` will be sent — the display name `John Doe` will not appear in the delivered email's `To` header.

The sender (`from`) supports display names via the `{"address": "...", "name": "..."}` format.

**Maximum 50 recipients** combined across `to`, `cc`, and `bcc`. An `InvalidArgumentException` is thrown before the API call if this limit is exceeded.

**Single `Reply-To` address.** If multiple reply-to addresses are set, only the first one is sent.

Domain setup
------------

[](#domain-setup)

Before sending, your domain must be onboarded in Cloudflare Email Sending. Cloudflare will add the necessary DNS records (MX, SPF, DKIM, DMARC) automatically. See the [official documentation](https://developers.cloudflare.com/email-service/get-started/send-emails/#set-up-your-domain).

Development
-----------

[](#development)

```
composer install
vendor/bin/phpunit
```

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance94

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

26d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/4db434a8e3b26741b9f1af524b19d88eba977f9b8cdea1f4f4821b76686afc07?d=identicon)[JulienRamel](/maintainers/JulienRamel)

---

Top Contributors

[![JulienRamel](https://avatars.githubusercontent.com/u/1608342?v=4)](https://github.com/JulienRamel "JulienRamel (2 commits)")

---

Tags

symfonyemailmailertransportcloudflaretransactionalemail-service

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/julienramel-cloudflare-mailer/health.svg)

```
[![Health](https://phpackages.com/badges/julienramel-cloudflare-mailer/health.svg)](https://phpackages.com/packages/julienramel-cloudflare-mailer)
```

###  Alternatives

[sylius/mailer-bundle

Mailers and e-mail template management for Symfony projects.

728.5M82](/packages/sylius-mailer-bundle)[mailersend/laravel-driver

MailerSend Laravel Driver

90820.0k7](/packages/mailersend-laravel-driver)[hafael/azure-mailer-driver

Supercharge your Laravel or Symfony app with Microsoft Azure Communication Services (ACS)! Effortlessly add email, chat, voice, video, and telephony-over-IP for next-level communication. 🚀

15122.2k](/packages/hafael-azure-mailer-driver)[dotkernel/dot-mail

Dotkernel mail component based on symfony mailer

1143.5k6](/packages/dotkernel-dot-mail)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
