draliragab / filament-cloudflare-mail-sender
A Filament panel package for composing and auditing transactional email sent through Laravel's Cloudflare mail transport.
Package info
github.com/DrAliRagab/filament-cloudflare-mail-sender
pkg:composer/draliragab/filament-cloudflare-mail-sender
Requires
- php: ^8.4
- filament/filament: ^5.1
- illuminate/console: ^13.23
- illuminate/contracts: ^13.23
- illuminate/database: ^13.23
- illuminate/mail: ^13.23
- illuminate/queue: ^13.23
- illuminate/support: ^13.23
- symfony/http-client: ^8.0
Requires (Dev)
- driftingly/rector-laravel: ^2.5
- larastan/larastan: ^3.0
- laravel/pint: ^1.24
- orchestra/testbench: ^11.1
- pestphp/pest: ^5.0.1
- pestphp/pest-plugin-laravel: ^5.0.1
- rector/rector: ^2.2.14
README
Compose, queue, and audit transactional email from a Filament panel — using Laravel’s built-in Cloudflare Email Sending mail transport.
No custom SMTP client. No marketing/campaign tools. One Send email action that validates, stores an audit record, and queues delivery.
| Requirement | Version |
|---|---|
| PHP | 8.4+ (8.x) |
| Laravel | 13.23+ (13.x) |
| Filament | 5.1+ (5.x) |
Quick start
1. Install
composer require draliragab/filament-cloudflare-mail-sender
2. Publish config + migration
php artisan vendor:publish --tag=filament-cloudflare-mail-sender-config php artisan vendor:publish --tag=filament-cloudflare-mail-sender-migrations php artisan migrate
The migration is publish-only so your app owns the schema history.
3. Configure Cloudflare in Laravel
- In Cloudflare, onboard the sending domain for Email Sending.
- Create an API token with Email Sending: Edit for that account.
- Put credentials in
.env(never commit them):
CLOUDFLARE_ACCOUNT_ID=your-account-id CLOUDFLARE_API_TOKEN=your-api-token MAIL_MAILER=cloudflare MAIL_FROM_ADDRESS=noreply@your-onboarded-domain.com MAIL_FROM_NAME="${APP_NAME}"
- Register Laravel’s native Cloudflare mailer in
config/mail.php:
'mailers' => [ // ... 'cloudflare' => [ 'transport' => 'cloudflare', ], ],
- Wire credentials in
config/services.php(tokenis preferred in Laravel 13):
'cloudflare' => [ 'account_id' => env('CLOUDFLARE_ACCOUNT_ID'), 'token' => env('CLOUDFLARE_API_TOKEN'), ],
This package does not ship its own Cloudflare HTTP client. It uses Laravel’s first-party cloudflare transport (Symfony HTTP Client 8).
4. Register the Filament plugin
use DrAliRagab\FilamentCloudflareMailSender\FilamentCloudflareMailSenderPlugin; use Filament\Panel; use Illuminate\Contracts\Auth\Authenticatable; public function panel(Panel $panel): Panel { return $panel // ... ->plugin( FilamentCloudflareMailSenderPlugin::make() ->authorizeComposeUsing( fn (Authenticatable $user): bool => $user->can('send transactional email'), ) ->authorizeAuditUsing( fn (Authenticatable $user): bool => $user->can('view transactional email audit'), ), ); }
Guests are always denied. If you omit a callback, any authenticated panel user is allowed — so set both callbacks on privileged panels.
5. Run a queue worker
Sends are queued by default. Without a worker, messages stay queued and never leave for Cloudflare.
php artisan queue:work
To use a dedicated queue name, set queue.name in the published package config (and pass --queue= to the worker). Leave queue.connection as null so it uses your app’s QUEUE_CONNECTION / queue.default at runtime.
6. Send from the panel
Open Email → Compose Email, fill recipients and Markdown body, then confirm Send email.
- The form validates and queues the message.
- Sent Emails is the audit log (status, envelope, bodies, failures).
- Status Accepted by Cloudflare means Laravel got a successful provider response — not inbox delivery.
Package configuration (common knobs)
Published file: config/filament-cloudflare-mail-sender.php.
Sender (From)
| Option | Default | Meaning |
|---|---|---|
from.address / from.name |
null |
Fall back to mail.from at runtime |
from.editable |
true |
Allow editing From in the compose UI |
from.allowed_domains |
[] |
When editable, limit domains (empty = default sender’s domain) |
Keep published from.address / from.name as null unless you intentionally override Laravel’s mail defaults. Calling config('mail.*') inside the published package config file is unsafe (load order).
Recipients
Suggestions come from your user model — they are not an allowlist. Any valid address can be entered.
'recipients' => [ 'model' => App\Models\User::class, 'email_column' => 'email', 'name_column' => 'name', 'suggestion_limit' => 250, ],
To / CC / BCC are normalized and deduplicated. Cloudflare allows at most 50 unique recipients per message; the package checks that before queueing.
Attachments
| Option | Default | Meaning |
|---|---|---|
attachments.enabled |
true |
Set false to hide uploads and skip disk checks |
attachments.disk |
null |
Falls back to filesystems.default at runtime |
attachments.max_file_kib |
4096 |
Per-file limit |
message.max_total_kib |
5120 |
Whole-message budget (Cloudflare-oriented) |
Navigation
Defaults to an Email nav group with Compose Email and Sent Emails. Override labels/group/icon/sort under navigation.
Audit retention
Retention is opt-in (audit.retention_days = null means never prune). Example for 90 days:
'audit' => [ 'store_body' => true, 'retention_days' => 90, ],
php artisan cloudflare-mail:prune
Schedule in routes/console.php:
use Illuminate\Support\Facades\Schedule; Schedule::command('cloudflare-mail:prune')->daily();
With audit.store_body = false, Markdown/HTML/plain text are scrubbed after a terminal status.
How sending works
- User confirms Send email.
- Package validates the form, renders Markdown → HTML + plain text (same renderer used for delivery).
- An audit row is stored, a send token is assigned, status becomes
queued, andSendComposedEmailis dispatched. - The worker sends via
Mail::mailer(...)and Laravel’s Cloudflare transport. - On success, status becomes Accepted by Cloudflare (
sent).
There is no separate save-draft / preview step in the UI. Double-clicks against the same compose scope are designed not to create duplicate provider sends.
Important limits of the Laravel transport
sent≠ delivered to the inbox.- No durable Cloudflare message ID is exposed to this package today.
- If Cloudflare accepts a message and the worker dies before local success is recorded, a rare duplicate send is possible — design content to tolerate that.
Transactional use only: no newsletters, campaigns, audience tools, or bulk marketing features.
Privacy
Audit logs can hold recipient addresses, subjects, bodies, and attachment metadata. Restrict who can compose and who can view Sent Emails. Credentials are never stored in package tables. Failure details are sanitized before persistence.
Development (package contributors)
Tests use fake / non-network mailers and never call Cloudflare.
composer format composer refactor composer analyse composer test composer verify # authoritative gate
Optional real-provider smoke test
Opt-in only — never in CI. After configuring a non-production Cloudflare account:
RUN_CLOUDFLARE_MAIL_SMOKE=1 php artisan tinker
use Illuminate\Mail\Message; use Illuminate\Support\Facades\Mail; throw_unless(($_SERVER['RUN_CLOUDFLARE_MAIL_SMOKE'] ?? null) === '1'); Mail::mailer('cloudflare')->raw( 'Manual Cloudflare transport smoke test.', static fn (Message $message) => $message ->to('you@example.com') ->subject('Manual Cloudflare mail smoke test'), );
Success means Cloudflare accepted the request, not that mail landed in an inbox.
License
MIT — see LICENSE.md.