cloudflare-email 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -1
- data/README.md +63 -369
- data/SECURITY.md +1 -9
- data/app/controllers/cloudflare/email/ingress_controller.rb +1 -2
- data/app/controllers/cloudflare/email/management/mailboxes_controller.rb +2 -166
- data/app/controllers/cloudflare/email/management/styles_controller.rb +2 -6
- data/docs/activerecord-tenanted.md +0 -11
- data/docs/custom-ingress.md +6 -16
- data/docs/delivery-events.md +2 -5
- data/docs/features.md +49 -123
- data/docs/getting-started.md +91 -172
- data/docs/mailboxes.md +27 -56
- data/docs/management-engine.md +1 -3
- data/docs/outbox.md +5 -6
- data/docs/reference.md +190 -0
- data/docs/routing-diagnostics.md +7 -2
- data/docs/thread-correlation.md +2 -4
- data/docs/troubleshooting.md +4 -4
- data/lib/cloudflare/email/active_record/base.rb +3 -53
- data/lib/cloudflare/email/client.rb +23 -5
- data/lib/cloudflare/email/engine.rb +1 -13
- data/lib/cloudflare/email/envelope.rb +2 -12
- data/lib/cloudflare/email/error.rb +5 -12
- data/lib/cloudflare/email/ingress.rb +19 -8
- data/lib/cloudflare/email/mailboxes/configuration.rb +7 -15
- data/lib/cloudflare/email/mailboxes/inbound_retention.rb +1 -14
- data/lib/cloudflare/email/mailboxes/models.rb +8 -147
- data/lib/cloudflare/email/mailboxes/service.rb +7 -258
- data/lib/cloudflare/email/mailboxes.rb +1 -0
- data/lib/cloudflare/email/management/adapter.rb +3 -25
- data/lib/cloudflare/email/management/configuration.rb +4 -11
- data/lib/cloudflare/email/management/engine.rb +2 -0
- data/lib/cloudflare/email/tenancy.rb +2 -73
- data/lib/cloudflare/email/tenant_job_context.rb +2 -85
- data/lib/cloudflare/email/version.rb +1 -1
- data/lib/cloudflare-email.rb +6 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_mailboxes.rb +1 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_receiving_domains.rb +1 -1
- data/templates/deploy-to-cloudflare/README.md +25 -7
- data/templates/deploy-to-cloudflare/docs/domain-setup.md +118 -0
- data/templates/deploy-to-cloudflare/package.json +1 -0
- data/templates/deploy-to-cloudflare/scripts/check-subdomains.mjs +54 -0
- data/templates/deploy-to-cloudflare/test/subdomains.test.ts +51 -0
- data/templates/worker/README.md +11 -7
- data/templates/worker/docs/domain-setup.md +118 -0
- data/templates/worker/docs/durable-inbound.md +4 -16
- data/templates/worker/package.json +1 -0
- data/templates/worker/scripts/check-subdomains.mjs +54 -0
- data/templates/worker/test/subdomains.test.ts +51 -0
- metadata +22 -21
- data/app/views/cloudflare/email/management/mailboxes/index.html.erb +0 -65
- data/app/views/cloudflare/email/management/mailboxes/message.html.erb +0 -34
- data/app/views/cloudflare/email/management/mailboxes/show.html.erb +0 -86
- data/app/views/layouts/cloudflare/email/management.html.erb +0 -29
- data/docs/architecture.md +0 -105
- data/docs/tenant-mailboxes-plan.md +0 -149
- data/docs/upgrading-0.2.md +0 -101
- data/docs/upgrading-0.3.md +0 -55
- data/docs/verification/2026-09-10-followup.md +0 -49
- data/docs/verification/2026-09-10-install-upgrade.md +0 -86
- data/docs/verification/2026-09-10-live.md +0 -102
- data/docs/verification/2026-09-10.md +0 -175
- data/docs/verification/2026-09-11-custom-ingress.md +0 -49
- data/docs/verification/2026-09-11-extraction.md +0 -32
- data/docs/verification/2026-09-11-outbound-ledger.md +0 -60
- data/docs/verification/2026-09-11-security.md +0 -76
- data/docs/verification/2026-09-11-tenant-mailboxes.md +0 -75
- data/docs/verification/2026-09-12-destination-context.md +0 -46
- data/docs/verification/2026-09-13-durable-inbound-and-routing.md +0 -71
- data/lib/cloudflare/email/management/management.css +0 -68
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b53307e62ade70f522e0fcb5705e11552e9718914d02bea8754a11653b15349a
|
|
4
|
+
data.tar.gz: 692c09a19fcb4ccd9295ccb692a19bf02aba3176b107c8500e5450b100ce1df3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43da0e85cde52970e696d5a2dfd74f03c0f39f5ea7d66e33cd28ec2bb58e5872c57db7f5dbc010ab85ad43312de9c9abcf2eed31a65e01f78fed8b6de164483b
|
|
7
|
+
data.tar.gz: a38902643ce082d656a01b7d91a34e70f95f15d66de6fd06e1fe7502461ae6d155a64edcea780155dd9b00b055635a3b10d5a0e9d8cbf2d077e1a7782742b193
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,70 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 — 2026-10-06
|
|
4
|
+
|
|
5
|
+
- Report the provider's own reason when a refused request returns a body that
|
|
6
|
+
is not a Cloudflare JSON object. Cloudflare Queues throttles a pull with
|
|
7
|
+
`429` and the bare text `Queue is overloaded. Please back off.`; that raised
|
|
8
|
+
`RateLimitError: invalid JSON API response; request outcome is unknown`, which
|
|
9
|
+
dropped the reason and called a known refusal unknown. It now raises
|
|
10
|
+
`RateLimitError: HTTP 429: Queue is overloaded. Please back off.` The text is
|
|
11
|
+
collapsed to one line and capped at 200 characters; an HTML error page is
|
|
12
|
+
still never echoed. A `5xx` keeps "request outcome is unknown" because it may
|
|
13
|
+
have failed after taking effect, and an unreadable `2xx` still fails closed.
|
|
14
|
+
Error classes and retry behaviour are unchanged.
|
|
15
|
+
- Honour `Retry-After` on a `429` whose body is a bare JSON string. The parsed
|
|
16
|
+
string was not a Hash, so the header was never stashed and the retry slept
|
|
17
|
+
the default backoff instead.
|
|
18
|
+
- Describe an unreadable provider response as an unknown *request* outcome rather
|
|
19
|
+
than an unknown *delivery* outcome. `Client#request` serves every caller, so a
|
|
20
|
+
queue pull that timed out reported that a delivery outcome was unknown when no
|
|
21
|
+
message was in flight — sending operators to look for an email that never
|
|
22
|
+
existed.
|
|
23
|
+
|
|
24
|
+
This message reaches **every** caller of `Client#request`, sending included:
|
|
25
|
+
nothing wraps or rescues it in `DeliveryMethod` or `Outbox.deliver`, and
|
|
26
|
+
`SendJob`'s "delivery outcome is unknown; reconcile before retrying" is a
|
|
27
|
+
separate condition (retrying a delivery already in the `unknown` state), not a
|
|
28
|
+
wrapper around this error. A send caller now reads "request outcome is unknown",
|
|
29
|
+
which remains accurate for a send — the request *is* the send — and stops the
|
|
30
|
+
same string from asserting a delivery on the paths that never had one.
|
|
31
|
+
|
|
32
|
+
## 0.4.0 — 2026-09-14
|
|
33
|
+
|
|
34
|
+
Released alongside Mailbox Kit 0.1.0. Cloudflare installs the core automatically.
|
|
35
|
+
Existing installations should follow the [core upgrade guide](maintainer/upgrading-mailbox-kit.md).
|
|
36
|
+
|
|
37
|
+
- Allow receiving domains without a sending account. The upgrade generator
|
|
38
|
+
preserves existing directory records while making account ownership optional.
|
|
39
|
+
|
|
40
|
+
- Index inbox memberships by inbound email for recipient isolation checks,
|
|
41
|
+
retention and purge. New schemas include the index; `mailbox_kit:upgrade`
|
|
42
|
+
generates a data-preserving migration for existing installations.
|
|
43
|
+
- Delegate shared configuration to Mailbox Kit and isolate Cloudflare session
|
|
44
|
+
additions in an integration module. Provide a short core quickstart plus
|
|
45
|
+
packaged integration and upgrade guides.
|
|
46
|
+
|
|
47
|
+
- Lean on Action Mailbox for original email records, processing and configurable
|
|
48
|
+
retention. Add idempotent attachment of existing Rails records and source-based
|
|
49
|
+
receiving that resolves duplicates for inbox membership. Share the persistence
|
|
50
|
+
bridge with Cloudflare while preserving its signed delivery identity and legacy
|
|
51
|
+
duplicate return value. Reuse Rails' model load hook for selective retention.
|
|
52
|
+
|
|
53
|
+
- Extract provider-neutral mailbox storage, recipient routing, optional tenancy,
|
|
54
|
+
raw-message retention, Rails job context and the server-rendered UI into the
|
|
55
|
+
sibling `mailbox-kit` gem. Cloudflare APIs, Worker transport, sending outbox and
|
|
56
|
+
delivery feedback remain in `cloudflare-email`. Preserve existing table names,
|
|
57
|
+
public constants, job payload keys and mounted Cloudflare engine routes.
|
|
58
|
+
See [Mailbox Kit](mailbox-kit/README.md) for standalone setup and release order.
|
|
59
|
+
|
|
60
|
+
- Add a cited one-time domain setup guide for both receiving domains and dynamic
|
|
61
|
+
organization subdomains, shared between Worker templates. Add a read-only
|
|
62
|
+
`npm run check:subdomains` DNS helper with named/fresh probes and explicit limits
|
|
63
|
+
on what DNS can verify. The Worker transport and gem routing behavior are unchanged.
|
|
64
|
+
|
|
3
65
|
## 0.3.0 — 2026-09-13
|
|
4
66
|
|
|
5
|
-
See [upgrading from 0.2](
|
|
67
|
+
See [upgrading from 0.2](maintainer/upgrading-0.3.md) before deploying the updated Worker.
|
|
6
68
|
|
|
7
69
|
- Add a standalone Deploy to Cloudflare template with R2/Queue provisioning,
|
|
8
70
|
secret prompts, scheduled recovery, and logs. CI checks runtime/configuration
|
data/README.md
CHANGED
|
@@ -1,167 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Cloudflare Email
|
|
2
2
|
|
|
3
|
-
Ruby
|
|
3
|
+
Send and receive email in Ruby and Rails through [Cloudflare Email Service](https://developers.cloudflare.com/email-service/).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use your existing Rails mailers, receive messages through Action Mailbox, and add
|
|
6
|
+
persistent inboxes with [Mailbox Kit](mailbox-kit/README.md). SQLite is supported.
|
|
7
|
+
Database tenancy and the management UI are optional.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Deploy the inbound Worker with guided R2/Queue provisioning and secret setup. [Prepare Rails and connect your email address](templates/deploy-to-cloudflare/README.md).
|
|
10
|
-
|
|
11
|
-
## Start here
|
|
12
|
-
|
|
13
|
-
**Sending email?** Start with the [step-by-step Rails guide](docs/getting-started.md).
|
|
14
|
-
**Building a mailbox?** Follow the same guide through receiving, the SQLite-compatible outbox, and delivery tracking.
|
|
15
|
-
**Using plain Ruby?** Jump to [Plain Ruby](#plain-ruby); Rails and a database are optional.
|
|
9
|
+
## Install
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| [Getting started](docs/getting-started.md) | Install, send your first email, receive replies, and save reliable send operations |
|
|
21
|
-
| [Troubleshooting](docs/troubleshooting.md) | What to check when mail or delivery updates do not arrive |
|
|
22
|
-
| [Managed mailboxes](docs/mailboxes.md) | Create inboxes and aliases, read/archive mail, and send from a mailbox |
|
|
23
|
-
| [Management engine](docs/management-engine.md) | Mount an optional server-rendered mailbox UI using your app's authentication |
|
|
24
|
-
| [Custom ingress](docs/custom-ingress.md) | Reuse authentication, signed metadata, and tenant routing in your existing ingestion pipeline |
|
|
25
|
-
| [Routing diagnostics](docs/routing-diagnostics.md) | Inspect exact-domain DNS and Worker routes without changing infrastructure |
|
|
26
|
-
| [Reusable Worker pipeline](templates/worker/README.md#reuse-the-transport-in-an-existing-worker) | Keep custom backend and archive policies while sharing bounded email forwarding |
|
|
27
|
-
| [Durable inbound delivery](templates/worker/README.md#durable-inbound-delivery) | Store incoming mail in R2 and recover Rails outages with queued retries and scheduled recovery |
|
|
28
|
-
| [Routing delivery confirmation](docs/routing-deliveries.md) | Confirm qualifying normal-address deliveries using authenticated Routing analytics |
|
|
29
|
-
| [SQLite tenant databases](docs/activerecord-tenanted.md) | Give each organization its own SQLite database with `activerecord-tenanted` |
|
|
30
|
-
| [Durable outbox](docs/outbox.md) | Detailed setup, callbacks, retries, and recovery |
|
|
31
|
-
| [Delivery events](docs/delivery-events.md) | Cloudflare Queue setup and recipient status tracking |
|
|
32
|
-
| [Upgrading to 0.3](docs/upgrading-0.3.md) | Changes needed for an existing installation |
|
|
33
|
-
|
|
34
|
-
The sections below are the configuration and API reference.
|
|
11
|
+
```ruby
|
|
12
|
+
gem "cloudflare-email", "~> 0.4.0"
|
|
13
|
+
```
|
|
35
14
|
|
|
36
|
-
|
|
15
|
+
Mailbox Kit installs automatically. If you use another email provider, you can
|
|
16
|
+
[use Mailbox Kit on its own](mailbox-kit/README.md).
|
|
37
17
|
|
|
38
|
-
|
|
18
|
+
Requires Ruby 3.2+; Rails integrations support Rails 7.2–8.1.
|
|
19
|
+
Use a current patch release. Plain Ruby sending does not require Rails or a database.
|
|
39
20
|
|
|
40
|
-
|
|
41
|
-
gem "cloudflare-email", "~> 0.3.0"
|
|
42
|
-
```
|
|
21
|
+
## Send from Rails
|
|
43
22
|
|
|
44
23
|
```sh
|
|
45
24
|
bundle install
|
|
46
25
|
bin/rails generate cloudflare:email:install --no-inbound
|
|
47
26
|
```
|
|
48
27
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```yaml
|
|
52
|
-
cloudflare:
|
|
53
|
-
account_id: your-account-id
|
|
54
|
-
api_token: your-email-sending-token
|
|
55
|
-
```
|
|
28
|
+
[Verify a sending domain in Cloudflare](https://developers.cloudflare.com/email-service/configuration/domains/)
|
|
29
|
+
and add your account ID and sending token:
|
|
56
30
|
|
|
57
31
|
```sh
|
|
58
32
|
export CLOUDFLARE_ACCOUNT_ID=your-account-id
|
|
59
33
|
export CLOUDFLARE_API_TOKEN=your-email-sending-token
|
|
60
34
|
```
|
|
61
35
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Onboard a sending domain under **Compute → Email Service → Email Sending**. Use a dedicated sending subdomain if the apex already uses another mail provider. Cloudflare's onboarding adds the required bounce MX, SPF, DKIM, and DMARC records for that sending domain. Follow the current [domain configuration](https://developers.cloudflare.com/email-service/configuration/domains/) instructions; do not replace an existing apex SPF record or publish a second one.
|
|
65
|
-
|
|
66
|
-
Create a token with permission to send for your account, and verify the domain in the dashboard before testing:
|
|
67
|
-
|
|
68
|
-
```sh
|
|
69
|
-
bin/rails cloudflare:email:doctor
|
|
70
|
-
FROM=hello@mail.example.com TO=you@example.net bin/rails cloudflare:email:send_test
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
`doctor` checks configuration and available read access. It does not send mail or prove deliverability. `send_test` requires both `FROM` and `TO`.
|
|
74
|
-
|
|
75
|
-
Standard ActionMailer works:
|
|
36
|
+
Your regular mailers can now send through Cloudflare:
|
|
76
37
|
|
|
77
38
|
```ruby
|
|
78
|
-
class
|
|
79
|
-
def
|
|
80
|
-
mail(from: "hello@mail.example.com", to:
|
|
81
|
-
format.text { render plain: "Hello!" }
|
|
39
|
+
class HelloMailer < ApplicationMailer
|
|
40
|
+
def hello(address)
|
|
41
|
+
mail(from: "hello@mail.example.com", to: address, subject: "Hello") do |format|
|
|
42
|
+
format.text { render plain: "Hello from Rails!" }
|
|
82
43
|
end
|
|
83
44
|
end
|
|
84
45
|
end
|
|
85
46
|
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Multipart, attachments, cc/bcc, and threading headers are serialized through `send_raw`. Cloudflare still controls final delivery and header acceptance.
|
|
90
|
-
|
|
91
|
-
## Managed mailboxes and optional tenancy
|
|
92
|
-
|
|
93
|
-
**Multi-tenancy is off by default.** Installing the gem does not create tenant
|
|
94
|
-
databases, install a tenancy library, or change your application's database routing.
|
|
95
|
-
|
|
96
|
-
| Setup | What you explicitly enable |
|
|
97
|
-
| --- | --- |
|
|
98
|
-
| Plain Ruby sending | Nothing extra; no Rails/database required |
|
|
99
|
-
| Managed mailboxes in one database | Run the mailbox generator and load the optional module |
|
|
100
|
-
| Separate tenant databases | Configure `Tenancy.configure(...)` with your application's base class and switching adapter before loading models |
|
|
101
|
-
|
|
102
|
-
The mailbox API uses a tenant key to group and scope records even in one database.
|
|
103
|
-
That key alone does not enable database switching. `activerecord-tenanted` is an
|
|
104
|
-
optional application dependency, not a runtime dependency of this gem.
|
|
105
|
-
|
|
106
|
-
Create mailboxes in code with the optional mailbox module. It includes named
|
|
107
|
-
mailboxes, aliases, ownership references, read/archive state, retained raw mail,
|
|
108
|
-
mailbox-authorized sends, and tenant-aware delivery-event recovery:
|
|
109
|
-
|
|
110
|
-
```sh
|
|
111
|
-
bin/rails generate cloudflare:email:mailboxes
|
|
112
|
-
bin/rails db:migrate
|
|
47
|
+
HelloMailer.hello("you@example.net").deliver_now
|
|
113
48
|
```
|
|
114
49
|
|
|
115
|
-
|
|
50
|
+
HTML templates, attachments, multipart messages, cc/bcc, and reply headers work
|
|
51
|
+
through Action Mailer. Follow the [Rails setup guide](docs/getting-started.md)
|
|
52
|
+
for receiving and delivery tracking.
|
|
116
53
|
|
|
117
|
-
|
|
118
|
-
Cloudflare::Email::Mailboxes.for_tenant("organization-123") do |inboxes|
|
|
119
|
-
mailbox = inboxes.create(name: "Support", address: "support@acme.example.com",
|
|
120
|
-
owner_ref: "team:42")
|
|
121
|
-
# Provision and activate its address route before receiving or sending mail.
|
|
122
|
-
end
|
|
123
|
-
```
|
|
54
|
+
## Receive email
|
|
124
55
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
provides the UI. Start with [the mailbox guide](docs/mailboxes.md), and configure
|
|
129
|
-
[tenant connections](docs/activerecord-tenanted.md) before loading models when
|
|
130
|
-
using separate databases. Plain Ruby users do not load this module.
|
|
56
|
+
The included Worker stores incoming email at Cloudflare and retries delivery
|
|
57
|
+
while Rails is unavailable. Rails verifies each request and stores the original
|
|
58
|
+
email through Action Mailbox.
|
|
131
59
|
|
|
132
|
-
|
|
60
|
+
[](https://deploy.workers.cloudflare.com/?url=https://github.com/cole-robertson/cloudflare-email/tree/main/templates/deploy-to-cloudflare)
|
|
133
61
|
|
|
134
|
-
|
|
135
|
-
|
|
62
|
+
[Prepare Rails and deploy the Worker](templates/deploy-to-cloudflare/README.md),
|
|
63
|
+
then connect your receiving address in Cloudflare Email Routing.
|
|
136
64
|
|
|
137
|
-
|
|
138
|
-
bin/rails generate cloudflare:email:tracking
|
|
139
|
-
bin/rails generate cloudflare:email:outbox
|
|
140
|
-
bin/rails db:migrate
|
|
141
|
-
```
|
|
65
|
+
## Build an inbox
|
|
142
66
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
operation = Cloudflare::Email::ActiveRecord::Outbox.prepare_mail(
|
|
146
|
-
account_id: account,
|
|
147
|
-
operation_key: "reply:#{draft.id}:revision:#{draft.revision}",
|
|
148
|
-
mail: ReplyMailer.reply(draft).message,
|
|
149
|
-
)
|
|
150
|
-
# Enqueue after the transaction that prepared the operation commits.
|
|
151
|
-
Cloudflare::Email::SendJob.perform_later(account, operation.operation_key)
|
|
152
|
-
```
|
|
67
|
+
The [mailbox guide](docs/mailboxes.md) walks through creating inboxes and aliases,
|
|
68
|
+
receiving mail, marking it read, archiving it, and sending replies.
|
|
153
69
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
deduplicates, correlates, orders, and replays queue events, with transactional
|
|
158
|
-
callbacks for your product records. The reference Rails inbox uses these same
|
|
159
|
-
tables and APIs for compose, replies, approved AI drafts, and recovery.
|
|
70
|
+
Mount the optional [management UI](docs/management-engine.md) to manage mailboxes
|
|
71
|
+
and read messages using your app's login and permissions. It is server-rendered
|
|
72
|
+
and needs no frontend build.
|
|
160
73
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
setup and recovery guide](docs/outbox.md) and [gem/inbox architecture](docs/architecture.md).
|
|
164
|
-
The adapter is opt-in; the plain Ruby client does not load Rails or ActiveRecord.
|
|
74
|
+
For a runnable SQLite example, clone the
|
|
75
|
+
[hello-world Rails app](https://github.com/cole-robertson/cloudflare-email-rails-starter).
|
|
165
76
|
|
|
166
77
|
## Plain Ruby
|
|
167
78
|
|
|
@@ -174,250 +85,33 @@ client = Cloudflare::Email::Client.new(
|
|
|
174
85
|
)
|
|
175
86
|
|
|
176
87
|
response = client.send(
|
|
177
|
-
from:
|
|
88
|
+
from: "hello@mail.example.com",
|
|
178
89
|
to: "you@example.net",
|
|
179
90
|
subject: "Hello",
|
|
180
|
-
text: "
|
|
181
|
-
html: "<p>HTML body</p>",
|
|
182
|
-
reply_to: "support@in.example.com",
|
|
91
|
+
text: "Hello from Ruby!",
|
|
183
92
|
)
|
|
184
|
-
response.message_id
|
|
185
|
-
response.
|
|
186
|
-
response.queued # Recipients queued for later delivery
|
|
187
|
-
response.permanent_bounces # Recipients that permanently bounced
|
|
188
|
-
response.suppressed_recipients # Recipients dropped by suppression policy
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
`to` may be omitted for cc-only or bcc-only mail. Addresses accept strings or `{ address:, name: }` hashes. Attachments use Cloudflare's `content` (base64), `filename`, `type`, `disposition`, and optional `content_id` fields. For full MIME control, use `client.send_raw(from:, recipients:, mime_message:)`.
|
|
192
|
-
|
|
193
|
-
An HTTP success does not mean every recipient received the message. Inspect the recipient outcome arrays and use [delivery events](docs/delivery-events.md) for later results. The current API reference includes message IDs and suppressed recipients; older responses remain supported.
|
|
194
|
-
|
|
195
|
-
Cloudflare currently limits ordinary sends to 50 recipients and 5 MiB including attachments; verified destination addresses have a 25 MiB allowance. These limits are enforced by Cloudflare. See [limits](https://developers.cloudflare.com/email-service/platform/limits/) and the [header allowlist](https://developers.cloudflare.com/email-service/reference/headers/).
|
|
196
|
-
|
|
197
|
-
## Receive through ActionMailbox
|
|
198
|
-
|
|
199
|
-
Cloudflare Email Routing invokes an Email Worker. The bundled Worker forwards unchanged raw MIME to Rails with a versioned HMAC-SHA256 signature covering the timestamp, SMTP envelope, and body. Rails verifies the signature and a five-minute timestamp window before storing the message in ActionMailbox.
|
|
200
|
-
|
|
201
|
-
```sh
|
|
202
|
-
bin/rails generate cloudflare:email:install
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
The interactive installer offers ActionMailbox installation/migrations and a default `MainMailbox`, copies the Worker, and configures ingress in development and production. Add `--all-envs` to include test; configure custom staging environments explicitly. Send-only apps do not need ActionMailbox.
|
|
206
|
-
|
|
207
|
-
Add the generated ingress secret and an optional management token:
|
|
208
|
-
|
|
209
|
-
```yaml
|
|
210
|
-
cloudflare:
|
|
211
|
-
account_id: your-account-id
|
|
212
|
-
api_token: your-runtime-send-token
|
|
213
|
-
management_token: your-deployment-token
|
|
214
|
-
ingress_secret: the-generated-random-secret
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Equivalent environment names are `CLOUDFLARE_MANAGEMENT_TOKEN` and `CLOUDFLARE_INGRESS_SECRET`. Keep management credentials in the deployment environment rather than the running application. Credentials stored in the application's Rails credentials are accessible to that application; this gem is not a secret-isolation boundary.
|
|
218
|
-
|
|
219
|
-
1. Add the receiving subdomain, for example `in.example.com`, in **Email Routing → apex domain → Settings → Subdomains**. This is a separate onboarding step from sending.
|
|
220
|
-
2. Deploy the environment's Worker and create its route:
|
|
221
|
-
|
|
222
|
-
First create the private R2 bucket and Queue, then deploy the generated Wrangler
|
|
223
|
-
configuration using the [durable setup guide](templates/worker/docs/durable-inbound.md).
|
|
224
|
-
The Ruby task below updates that provisioned Worker while preserving its bindings.
|
|
225
|
-
It refuses a missing bucket/queue binding or recovery schedule before changing code.
|
|
226
|
-
|
|
227
|
-
```sh
|
|
228
|
-
RAILS_ENV=production bin/rails cloudflare:email:deploy_worker URL=https://app.example.com/rails/action_mailbox/cloudflare/inbound_emails
|
|
229
|
-
RAILS_ENV=production bin/rails cloudflare:email:provision_route ADDRESS=support@in.example.com
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
Subdomain provisioning checks configured DNS before creating a rule. It never enables the parent apex on behalf of a subdomain. A missing setup or permission fails with instructions. DNS records alone do not prove propagation or live routing; send a real test afterward.
|
|
233
|
-
|
|
234
|
-
For a zone apex, provisioning may enable Email Routing and its DNS records. Only use this when Cloudflare should handle mail for that apex. `provision_catchall DOMAIN=example.com` changes the **zone-wide** catch-all; a subdomain that resolves to a parent zone is rejected. See [subdomain onboarding](https://developers.cloudflare.com/email-service/configuration/subdomains/).
|
|
235
|
-
|
|
236
|
-
Replace the scaffolded mailbox's `process` with your application logic. For tenant or mailbox selection, use the authenticated SMTP recipient rather than the sender-controlled MIME `To`/`Cc` headers:
|
|
237
|
-
|
|
238
|
-
```ruby
|
|
239
|
-
class ApplicationMailbox < ActionMailbox::Base
|
|
240
|
-
routing ->(inbound) { Cloudflare::Email::Envelope.for(inbound)&.fetch("to")&.match?(/\Asupport@/i) } => :support
|
|
241
|
-
end
|
|
93
|
+
response.message_id
|
|
94
|
+
response.accepted?
|
|
242
95
|
```
|
|
243
96
|
|
|
244
|
-
|
|
97
|
+
Acceptance is the start of delivery. Use [delivery events](docs/delivery-events.md)
|
|
98
|
+
to track delivered messages, bounces, and complaints.
|
|
99
|
+
See the [Ruby API reference](docs/reference.md) for attachments, raw MIME, and configuration.
|
|
245
100
|
|
|
246
|
-
|
|
101
|
+
## Guides
|
|
247
102
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
For a custom installer `--worker-dir`, pass `SCRIPT=custom-directory/src/index.js` to the Ruby `deploy_worker` task. The installer prints the corresponding command.
|
|
264
|
-
|
|
265
|
-
Ruby deployment and Wrangler use matching names: `cloudflare-email-ingress-development`, `-staging`, and `-production`. Initial durable infrastructure deployment uses Wrangler and requires Node 22.12+ (or a supported newer version):
|
|
266
|
-
|
|
267
|
-
```sh
|
|
268
|
-
cd cloudflare-worker
|
|
269
|
-
npm ci
|
|
270
|
-
npx wrangler secret put INGRESS_SECRET --env production
|
|
271
|
-
npx wrangler secret put RAILS_INGRESS_URL --env production
|
|
272
|
-
npm run deploy -- --env production
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
See the [Worker README](templates/worker/README.md). Existing deployments/templates are not automatically migrated; verify routing before switching names.
|
|
276
|
-
|
|
277
|
-
Rotate the shared ingress secret in Rails and the corresponding Worker during a coordinated deployment. This version has no overlapping-key rotation window; requests can fail while secrets differ.
|
|
278
|
-
|
|
279
|
-
## Outbound delivery events
|
|
280
|
-
|
|
281
|
-
The new `DeliveryEvent` and `EventConsumer` APIs consume Cloudflare Email Sending lifecycle events through an HTTP pull queue: delivered, deferred, bounced, failed, rejected, and complained.
|
|
282
|
-
|
|
283
|
-
```ruby
|
|
284
|
-
consumer = Cloudflare::Email::EventConsumer.new(
|
|
285
|
-
account_id: ENV.fetch("CLOUDFLARE_ACCOUNT_ID"),
|
|
286
|
-
api_token: ENV.fetch("CLOUDFLARE_QUEUES_TOKEN"),
|
|
287
|
-
queue_id: ENV.fetch("CLOUDFLARE_EVENT_QUEUE_ID"),
|
|
288
|
-
domains: ["mail.example.com"],
|
|
289
|
-
)
|
|
290
|
-
consumer.poll do |event|
|
|
291
|
-
DeliveryEventProcessor.call(event) # Your durable, idempotent application handler
|
|
292
|
-
end
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
Each event is acknowledged only after the handler returns normally. Configure a dedicated queue, subscription, retry policy, and dead-letter queue first. See the complete [Rails and Ruby delivery-event setup](docs/delivery-events.md).
|
|
296
|
-
|
|
297
|
-
Rails applications can run `bin/rails generate cloudflare:email:tracking` and
|
|
298
|
-
`bin/rails db:migrate` to install optional durable event receipts. The adapter
|
|
299
|
-
commits before ACK, deduplicates account/event IDs, retains unmatched events,
|
|
300
|
-
and replays them with transactional application handlers. It requires explicit
|
|
301
|
-
opt-in; the plain Ruby client stays independent of ActiveRecord.
|
|
302
|
-
|
|
303
|
-
Shared helpers include `Response#accepted?` (any provider acceptance, not final
|
|
304
|
-
delivery) and `DeliveryEvent#supersedes?(occurred_at:, terminal:)` for ordering
|
|
305
|
-
matched recipient events. See the [architecture guide](docs/architecture.md) for
|
|
306
|
-
what the gem provides and what belongs in the inbox product.
|
|
307
|
-
|
|
308
|
-
Applications using the outbox can use `DeliveryEvents.record(event)` and
|
|
309
|
-
`DeliveryEvents.replay(account_id:, message_id:)` for built-in correlation and
|
|
310
|
-
recipient-state projection. Applications with their own delivery schema can
|
|
311
|
-
continue to use the lower-level `EventInbox` with an explicit handler.
|
|
312
|
-
|
|
313
|
-
## Thread correlation
|
|
314
|
-
|
|
315
|
-
Prefer storing the provider's returned `message_id` with your conversation and correlating inbound `In-Reply-To` / `References` against that record. Correlation does not authenticate the sender or authorize an action.
|
|
316
|
-
|
|
317
|
-
Use `Cloudflare::Email::MessageId.normalize(value)` to trim whitespace and one enclosing angle-bracket pair without changing the ID's case. The old `SecureMessageId` helper has been removed.
|
|
318
|
-
|
|
319
|
-
Cloudflare's current [header documentation](https://developers.cloudflare.com/email-service/reference/headers/) describes Message-ID as platform-controlled. **The September 10 live test confirmed that Cloudflare replaced custom signed IDs**, including raw-MIME and ActionMailer sends. Store the provider ID for Cloudflare reply correlation. See [thread correlation](docs/thread-correlation.md) and the [live evidence](docs/verification/2026-09-10-live.md).
|
|
320
|
-
|
|
321
|
-
## Retry and configuration
|
|
322
|
-
|
|
323
|
-
`Client.new` options are also accepted by `config.action_mailer.cloudflare_settings`:
|
|
324
|
-
|
|
325
|
-
| Option | Default |
|
|
326
|
-
|---|---|
|
|
327
|
-
| `account_id`, `api_token` | Required |
|
|
328
|
-
| `base_url` | `https://api.cloudflare.com/client/v4` |
|
|
329
|
-
| `timeout` | 30 seconds for open/read/write |
|
|
330
|
-
| `total_timeout` | Defaults to `timeout`; bounds one complete HTTP attempt, including headers and streamed body |
|
|
331
|
-
| `max_response_bytes` | 1 MiB for `Client`; 32 MiB for `EventConsumer` queue batches |
|
|
332
|
-
| `retries` | 3 additional attempts |
|
|
333
|
-
| `initial_backoff` | 0.5 seconds, doubling |
|
|
334
|
-
| `max_retry_after` | 60 seconds |
|
|
335
|
-
| `retry_ambiguous` | `false` |
|
|
336
|
-
| `logger` | `nil`, optional `warn` logger |
|
|
337
|
-
|
|
338
|
-
By default, only 429 responses and pre-send connection failures retry. Numeric and HTTP-date `Retry-After` values are honored up to the cap. Read/write timeouts, connection resets, and 5xx responses may occur after acceptance; they raise without automatically resending. Setting `retry_ambiguous: true` restores retries for those failures and can send duplicates.
|
|
339
|
-
|
|
340
|
-
Responses are streamed with a byte limit, including decompressed content and
|
|
341
|
-
error responses. A total deadline or response-size failure after transmission
|
|
342
|
-
does not prove a send failed: the outbox preserves the ambiguous claim and blocks
|
|
343
|
-
automatic resend. Net::HTTP's implicit retries are disabled. Explicit retries
|
|
344
|
-
and backoff can make a logical call longer than `total_timeout`; queue handlers,
|
|
345
|
-
database transactions and a complete polling run have separate runtime budgets.
|
|
346
|
-
An uncertain queue acknowledgement permits receipt replay, never email resending.
|
|
347
|
-
|
|
348
|
-
No idempotency key is sent. Reusing Message-ID does not guarantee deduplication or exactly-once delivery. Account for ActiveJob's retry policy too: retrying the whole mailer job can resend even when this client's retries are disabled.
|
|
349
|
-
|
|
350
|
-
Errors inherit from `Cloudflare::Email::Error`: `ConfigurationError`, `AuthenticationError`, `ValidationError`, `RateLimitError`, `ServerError`, and `NetworkError`. API errors expose `status` and parsed `response`.
|
|
351
|
-
|
|
352
|
-
## Observability and permissions
|
|
353
|
-
|
|
354
|
-
Notifications: `cloudflare_email.send` / `send_raw` include `account_id`, `path`, `status`, `message_id`, and all four recipient outcome arrays. `cloudflare_email.ingress` includes `bytes`, `result` (`ok`, `duplicate`, `bad_signature`, `stale`, `too_large`), and the stored `message_id` when available. `cloudflare_email.delivery_event` wraps handler execution with `event_id`, `message_id`, and lifecycle `status`; it does not report queue acknowledgement completion. Instrumentation errors include ActiveSupport's exception metadata.
|
|
355
|
-
|
|
356
|
-
| Task | Purpose / credentials |
|
|
357
|
-
|---|---|
|
|
358
|
-
| `doctor` | Read diagnostics with runtime token; limited read permissions are reported |
|
|
359
|
-
| `send_test FROM=... TO=...` | Send a real message using runtime send permission |
|
|
360
|
-
| `deploy_worker URL=https://...` | Management token: Workers Scripts Edit |
|
|
361
|
-
| `provision_route ADDRESS=...` | Management token: Zone Read, Email Routing Rules Edit; DNS Read for subdomain checks; routing-settings write permission for apex enablement |
|
|
362
|
-
| `provision_catchall DOMAIN=...` | Same routing management permissions; changes the zone-wide catch-all |
|
|
363
|
-
| `dev` | Management token: Workers Scripts Edit; development only |
|
|
364
|
-
| `consume_events` | Separate Queues Read/Write token, queue ID, configured handler |
|
|
365
|
-
| `deliver OPERATION_KEY=...` | Dispatch a saved outbox operation using sending credentials |
|
|
366
|
-
| `replay_events [MESSAGE_ID=...]` | Replay durable receipts for the configured account |
|
|
367
|
-
| `pending_deliveries` | List prepared or uncertain outbox operations for operator review |
|
|
368
|
-
|
|
369
|
-
The optional Rails adapter emits `cloudflare_email.outbox_prepare`,
|
|
370
|
-
`cloudflare_email.outbox_send`, and `cloudflare_email.outbox_reconcile` notifications
|
|
371
|
-
with operation identity and resulting state. These do not contain MIME or API
|
|
372
|
-
tokens. A notification is method instrumentation; an enclosing application
|
|
373
|
-
transaction may still roll back. Inspect the durable ledger for authoritative state.
|
|
374
|
-
|
|
375
|
-
Management tasks fall back to the runtime token if `management_token` is unset. Restrict scopes and accounts to the operations you need. Event consumers use a separate `queues_token` and do not fall back to a send token.
|
|
376
|
-
|
|
377
|
-
## SMTP alternative
|
|
378
|
-
|
|
379
|
-
Cloudflare also supports authenticated SMTP. Existing Rails SMTP applications can use it without this gem's delivery method:
|
|
380
|
-
|
|
381
|
-
```ruby
|
|
382
|
-
config.action_mailer.delivery_method = :smtp
|
|
383
|
-
config.action_mailer.smtp_settings = {
|
|
384
|
-
address: "smtp.mx.cloudflare.net",
|
|
385
|
-
port: 465,
|
|
386
|
-
ssl: true,
|
|
387
|
-
authentication: :plain,
|
|
388
|
-
user_name: "api_token",
|
|
389
|
-
password: ENV.fetch("CLOUDFLARE_SMTP_TOKEN"),
|
|
390
|
-
}
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
Cloudflare documents Email Sending Edit permission for SMTP, implicit TLS on port 465, and no outbound STARTTLS on 587. See [SMTP documentation](https://developers.cloudflare.com/email-service/api/send-emails/smtp/). Remove the generated `:cloudflare` initializer override if switching to SMTP.
|
|
394
|
-
|
|
395
|
-
## Development and verification
|
|
396
|
-
|
|
397
|
-
```sh
|
|
398
|
-
bundle install
|
|
399
|
-
bundle exec rake test
|
|
400
|
-
bundle exec ruby script/verify_package.rb
|
|
401
|
-
BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle install
|
|
402
|
-
BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test
|
|
403
|
-
cd templates/worker
|
|
404
|
-
npm ci
|
|
405
|
-
npm test
|
|
406
|
-
npm run check
|
|
407
|
-
npm audit
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
Tests include actual Rails boot/installation, ActionMailbox persistence and mailbox processing, task orchestration, HTTP-mocked API behavior, and Worker unit tests. CI also builds and installs the packaged gem and runs a real local workerd-to-Rails check. Run the latter with:
|
|
411
|
-
|
|
412
|
-
```sh
|
|
413
|
-
BUNDLE_GEMFILE=gemfiles/local_ingress.gemfile bundle install
|
|
414
|
-
BUNDLE_GEMFILE=gemfiles/local_ingress.gemfile bundle exec ruby script/verify_local_ingress.rb
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
Install the Worker tooling first; Node 22+ must be on PATH (or set `NODE_BINARY` to its executable). This uses synthetic mail and temporary loopback services, not a deployed Cloudflare account. The development bundle pins JSON below 3 because current tested Rails versions require its positional-options API.
|
|
418
|
-
|
|
419
|
-
See the [verification report](docs/verification/2026-09-10.md) for evidence, the historical dogfood inventory, Rebulk integration findings, and remaining live-provider checks.
|
|
420
|
-
|
|
421
|
-
A subsequent [live verification pass](docs/verification/2026-09-10-live.md) exercised isolated sending, deployed ingress, binary attachments, reply threading, real LLM processing, and delivery-event redelivery/acknowledgement under `test.rebulk.com`. The [follow-up report](docs/verification/2026-09-10-followup.md) covers authenticated recipient routing and draft-review improvements after that run. Temporary cloud resources were removed afterward. No DNS changes or RubyGems publication occurred. These dated reports do not constitute live verification of later changes.
|
|
422
|
-
|
|
423
|
-
MIT license.
|
|
103
|
+
| Task | Guide |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| Set up sending, receiving, and tracking | [Rails quickstart](docs/getting-started.md) |
|
|
106
|
+
| Explore the available features | [Feature list](docs/features.md) |
|
|
107
|
+
| Create inboxes and aliases | [Mailboxes](docs/mailboxes.md) |
|
|
108
|
+
| Add mailbox administration | [Management UI](docs/management-engine.md) |
|
|
109
|
+
| Configure addresses and customer subdomains | [Cloudflare domain setup](templates/worker/docs/domain-setup.md) |
|
|
110
|
+
| Save outgoing messages and safely retry jobs | [Outbox](docs/outbox.md) |
|
|
111
|
+
| Track delivery and bounces | [Delivery events](docs/delivery-events.md) |
|
|
112
|
+
| Use separate SQLite databases | [Database tenancy](docs/activerecord-tenanted.md) |
|
|
113
|
+
| Connect a custom receiving endpoint | [Custom ingress](docs/custom-ingress.md) |
|
|
114
|
+
| Diagnose missing mail | [Troubleshooting](docs/troubleshooting.md) |
|
|
115
|
+
| Configure the client and command-line tasks | [Reference](docs/reference.md) |
|
|
116
|
+
|
|
117
|
+
[Security](SECURITY.md) · [Contributing](CONTRIBUTING.md) · [MIT license](LICENSE.txt)
|
data/SECURITY.md
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# Security
|
|
2
2
|
|
|
3
|
-
The hardening described here is included in 0.2.0. Version 0.1.0 does not include
|
|
4
|
-
these fixes. Upgrade Rails and the bundled Worker together using the
|
|
5
|
-
[upgrade guide](docs/upgrading-0.2.md).
|
|
6
|
-
|
|
7
3
|
Rails integration is tested on patched Rails 7.2, 8.0, and 8.1; application
|
|
8
4
|
owners must update their own Rails, database adapter, and other dependencies.
|
|
9
5
|
The gem does not add Rails or SQLite as runtime dependencies.
|
|
@@ -40,12 +36,8 @@ Do not place secrets or private message data in public issues.
|
|
|
40
36
|
Ingress selects a registered tenant only after full signature verification.
|
|
41
37
|
Raw ActiveRecord access remains privileged, especially in shared-database mode.
|
|
42
38
|
Private tenant storage, consistent migrations and trusted job payloads are
|
|
43
|
-
required.
|
|
44
|
-
without the new tenant metadata are rejected rather than guessing a tenant.
|
|
39
|
+
required. Jobs without the required tenant context are rejected.
|
|
45
40
|
- Development tunnels expose the ingress to the internet. Use the bundled task,
|
|
46
41
|
dedicated development mail routes and test data; stop tunnels when finished.
|
|
47
42
|
The Host guard restricts routing but is not a sandbox for the development app
|
|
48
43
|
or a replacement for safe error pages and request logging.
|
|
49
|
-
|
|
50
|
-
See [the September 11 security review](docs/verification/2026-09-11-security.md)
|
|
51
|
-
for findings, verification, and remaining limits.
|
|
@@ -35,9 +35,8 @@ module Cloudflare
|
|
|
35
35
|
head :payload_too_large
|
|
36
36
|
when :ok
|
|
37
37
|
inbound = if defined?(Cloudflare::Email::Mailboxes) && Cloudflare::Email::Mailboxes.enabled?
|
|
38
|
-
recipient = result.message.envelope.fetch("to")
|
|
39
38
|
begin
|
|
40
|
-
|
|
39
|
+
result.message.receive_into_mailbox!
|
|
41
40
|
rescue Cloudflare::Email::Mailboxes::Unavailable
|
|
42
41
|
payload[:result] = :unavailable_mailbox
|
|
43
42
|
next head(:unprocessable_entity)
|