cloudflare-email 0.4.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -2
  3. data/README.md +63 -379
  4. data/SECURITY.md +1 -9
  5. data/docs/activerecord-tenanted.md +0 -11
  6. data/docs/custom-ingress.md +1 -15
  7. data/docs/delivery-events.md +2 -5
  8. data/docs/features.md +49 -123
  9. data/docs/getting-started.md +90 -172
  10. data/docs/mailboxes.md +24 -60
  11. data/docs/management-engine.md +1 -3
  12. data/docs/outbox.md +5 -6
  13. data/docs/reference.md +190 -0
  14. data/docs/routing-diagnostics.md +0 -2
  15. data/docs/thread-correlation.md +2 -4
  16. data/docs/troubleshooting.md +4 -4
  17. data/lib/cloudflare/email/client.rb +23 -5
  18. data/lib/cloudflare/email/version.rb +1 -1
  19. data/templates/deploy-to-cloudflare/README.md +8 -10
  20. data/templates/deploy-to-cloudflare/docs/domain-setup.md +86 -166
  21. data/templates/worker/README.md +6 -8
  22. data/templates/worker/docs/domain-setup.md +86 -166
  23. data/templates/worker/docs/durable-inbound.md +4 -16
  24. metadata +2 -17
  25. data/docs/architecture.md +0 -105
  26. data/docs/tenant-mailboxes-plan.md +0 -149
  27. data/docs/upgrading-0.2.md +0 -101
  28. data/docs/upgrading-0.3.md +0 -55
  29. data/docs/verification/2026-09-10-followup.md +0 -49
  30. data/docs/verification/2026-09-10-install-upgrade.md +0 -86
  31. data/docs/verification/2026-09-10-live.md +0 -102
  32. data/docs/verification/2026-09-10.md +0 -175
  33. data/docs/verification/2026-09-11-custom-ingress.md +0 -49
  34. data/docs/verification/2026-09-11-extraction.md +0 -32
  35. data/docs/verification/2026-09-11-outbound-ledger.md +0 -60
  36. data/docs/verification/2026-09-11-security.md +0 -76
  37. data/docs/verification/2026-09-11-tenant-mailboxes.md +0 -75
  38. data/docs/verification/2026-09-12-destination-context.md +0 -46
  39. data/docs/verification/2026-09-13-action-mailbox-core.md +0 -96
  40. data/docs/verification/2026-09-13-durable-inbound-and-routing.md +0 -71
@@ -1,198 +1,118 @@
1
- # Set up once, create mailboxes in Rails
1
+ # Set up addresses and customer subdomains
2
2
 
3
- Use the same Worker for either address shape:
3
+ Choose the address pattern for your app:
4
4
 
5
- | Pattern | Example | What changes when a customer joins |
5
+ | Pattern | Example | Rails setup |
6
6
  | --- | --- | --- |
7
- | One receiving domain | `acme@in.example.com` | Create a mailbox/address in Rails |
8
- | Organization subdomains | `invoices@acme.in.example.com` | Register the exact organization domain and its mailboxes in Rails, within your verified receiving namespace |
9
-
10
- Neither pattern needs a recipient allowlist in the Worker. Rails owns accepted
11
- addresses and organization membership. Database multi-tenancy remains optional.
12
- The [Rails hello-world template](https://github.com/cole-robertson/cloudflare-email-rails-starter)
13
- demonstrates the first pattern with a normal SQLite database and generated login.
14
-
15
- ## What is verified, and what Cloudflare documents
16
-
17
- **Rebulk's existing deployment uses the second pattern:**
18
- `<site>@<organization>.rebulk.com`, wildcard MX, a catch-all Worker rule, and Rails
19
- organization/mailbox lookup. New organizations do not need a Worker enrollment
20
- list or a routine per-organization Cloudflare approval. Its September 2026
21
- rehearsal checked exact-domain public MX answers and archived SMTP-envelope
22
- delivery evidence, alongside the global catch-all rule. The apex keeps its
23
- separate mail provider. Sources (Rebulk repository access required):
24
-
25
- - [Worker architecture and organization setup](https://github.com/Rebulk/rebulk-system/blob/1cc26e0076ba0e9157e89f5879604df8c94430bc/cloudflare/README.md).
26
- - [Wildcard MX observations and limits of zone-level API evidence](https://github.com/Rebulk/rebulk-system/blob/1cc26e0076ba0e9157e89f5879604df8c94430bc/docs/cloudflare-mailbox-onboarding-rehearsal-2026-09-12.md).
27
-
28
- This is deployment evidence, not a Cloudflare-wide service guarantee. Cloudflare's
29
- [subdomain onboarding guide](https://developers.cloudflare.com/email-service/configuration/subdomains/)
30
- describes adding subdomains explicitly and currently lists a 30-domain combined
31
- Routing/Sending limit. It does not document arbitrary wildcard Email Routing as
32
- a way to bypass that limit. Cloudflare separately documents
33
- [wildcard DNS and exact-record precedence](https://developers.cloudflare.com/dns/manage-dns-records/reference/wildcard-dns-records/).
34
- Working wildcard DNS alone does not establish Email Routing acceptance. If your
35
- account does not accept unlisted subdomains, use documented explicit onboarding
36
- within its limits, or the single-domain pattern. Do not assume that an existing
37
- Rebulk setup proves a fresh account's behavior.
38
-
39
- A fresh [public DNS observation](https://github.com/cole-robertson/cloudflare-email/blob/main/docs/verification/2026-09-13-wildcard-dns.json)
40
- also records Cloudflare MX answers for `test.rebulk.com` and two random subdomains
41
- without creating DNS records or adding those labels in Cloudflare. That report
42
- is public and deliberately marks `delivery_verified: false`; no email was sent
43
- by this DNS-only check.
44
-
45
- ## 1. Fill in your setup worksheet
46
-
47
- Use a dedicated receiving namespace when the apex already serves Workspace or
48
- Microsoft 365. Example values:
49
-
50
- | Setting | Your example value |
51
- | --- | --- |
52
- | Cloudflare zone | `example.com` |
53
- | Receiving base | `in.example.com` |
54
- | Organization domain | `acme.in.example.com` |
55
- | Worker | `cloudflare-email-ingress` |
56
- | Rails ingress URL | `https://app.example.com/rails/action_mailbox/cloudflare/inbound_emails` |
57
- | Shared ingress secret | Generate with `openssl rand -hex 32`; save privately in Rails and the Worker |
58
-
59
- Keep staging and production Workers, buckets, queues, secrets, and receiving
60
- namespaces separate. The standalone deploy-button template takes one environment
61
- per copy; the regular CLI template uses `--env production` or another explicit env.
62
-
63
- ## 2. Deploy the shared infrastructure
7
+ | One receiving domain | `acme@in.example.com` | Register the domain once; create a mailbox for each customer |
8
+ | Customer subdomains | `invoices@acme.in.example.com` | Register each exact customer domain and its mailboxes |
9
+
10
+ One Worker can serve all your accepted addresses. Rails looks up the mailbox;
11
+ you do not need to maintain an address list in the Worker.
12
+ Database multi-tenancy is optional.
13
+
14
+ ## 1. Deploy the Worker
64
15
 
65
16
  [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cole-robertson/cloudflare-email/tree/main/templates/deploy-to-cloudflare)
66
17
 
67
- Deploy Rails with gem 0.3+ first. The button copies the standalone Worker and
68
- prompts for `RAILS_INGRESS_URL` and `INGRESS_SECRET`. Its configuration includes
69
- private R2 storage, a Queue producer/consumer, once-per-minute recovery, and logs.
70
- Cloudflare documents [automatic R2/Queue provisioning and secret prompts](https://developers.cloudflare.com/workers/platform/deploy-buttons/).
71
- The button does not create email DNS, claim a domain, or register Rails mailboxes.
18
+ Deploy Rails first, then enter its HTTPS ingress URL and shared secret in the
19
+ Worker deployment form. Cloudflare provisions private R2 storage and a Queue.
20
+ Use separate deployments for development and production.
21
+
22
+ The [setup guide](https://github.com/cole-robertson/cloudflare-email/tree/main/templates/deploy-to-cloudflare)
23
+ covers the form and required resources.
24
+
25
+ ## 2. Connect your receiving domain
26
+
27
+ Use a subdomain such as `in.example.com` if your main domain already receives
28
+ mail through Google Workspace or Microsoft 365.
29
+
30
+ In Cloudflare Email Routing:
31
+
32
+ 1. [Add the receiving domain or subdomain](https://developers.cloudflare.com/email-service/configuration/subdomains/).
33
+ 2. Add the DNS records Cloudflare provides, preserving your main domain's mail records.
34
+ 3. Create a route with **Send to a Worker** and select your deployed Worker.
35
+ 4. To accept many local parts, configure the receiving domain's
36
+ [catch-all rule](https://developers.cloudflare.com/email-service/configuration/email-routing-addresses/#catch-all-rule)
37
+ to use that Worker.
72
38
 
73
- ## 3. Connect DNS and the Worker once
39
+ A Cloudflare catch-all gets email to your Worker. Rails still decides which
40
+ addresses are accepted. To put unknown local parts into an inbox, also enable
41
+ the gem's [mailbox catch-all](https://github.com/cole-robertson/cloudflare-email/blob/main/docs/mailboxes.md#receive-unregistered-local-parts-with-an-optional-catch-all).
74
42
 
75
- For **one domain**, follow Cloudflare's documented
76
- [Email Routing onboarding](https://developers.cloudflare.com/email-service/get-started/route-emails/)
77
- and configure its [catch-all action](https://developers.cloudflare.com/email-service/configuration/email-routing-addresses/#catch-all-rule)
78
- as **Send to a Worker**, selecting your deployed Worker. This avoids one provider
79
- rule per local part. Review the actual zone/domain scope and any explicit rules
80
- that take precedence before saving.
43
+ ## 3. Customer subdomains
81
44
 
82
- For a **Rebulk-style dynamic namespace**, first establish receiving on your account
83
- and retain the exact MX targets/priorities assigned by Cloudflare. Then, when
84
- reproducing and verifying wildcard receiving, the DNS template is:
45
+ Cloudflare documents explicit subdomain onboarding. If you want new customer
46
+ subdomains to work without adding each one in Cloudflare, verify wildcard
47
+ receiving on your account before relying on it.
85
48
 
86
- | DNS type | Name in zone `example.com` | Value |
49
+ For a receiving namespace under `in.example.com`, the DNS pattern is:
50
+
51
+ | Type | Name in zone `example.com` | Value |
87
52
  | --- | --- | --- |
88
- | MX | `*.in` | Each Cloudflare-assigned receiving MX target, with its assigned priority |
53
+ | MX | `*.in` | Each receiving MX target and priority assigned by Cloudflare |
89
54
 
90
- Create one record per assigned target. Do not invent MX targets, point MX at a
91
- Worker URL, or replace the apex's existing MX records. Complete the service's
92
- other required DNS records through its onboarding instructions; do not blindly
93
- duplicate SPF records. Inspect the account's catch-all **Send to a Worker** rule
94
- and verify that it actually handles your intended namespace. A Cloudflare Worker
95
- HTTP route such as `*.example.com/*` is unrelated to email routing.
55
+ Wildcard DNS alone does not guarantee Email Routing accepts an unlisted
56
+ subdomain. Test with a fresh customer subdomain and confirm email reaches the
57
+ Worker and the correct Rails inbox. If your account requires explicit onboarding,
58
+ add those subdomains in Cloudflare or use the single-domain pattern.
96
59
 
97
- An explicit DNS name can stop wildcard inheritance **even if its record is TXT
98
- or A rather than MX**. For example, an existing `acme.in.example.com` record may
99
- require its own receiving MX records. Test exact customer names as well as fresh
100
- ones. A wildcard also does not supply MX for the receiving base itself.
60
+ [Explicit DNS records override wildcard inheritance](https://developers.cloudflare.com/dns/manage-dns-records/reference/wildcard-dns-records/),
61
+ including names with A or TXT records. Check existing customer names as well as
62
+ new ones. The receiving base `in.example.com` needs its own records.
101
63
 
102
- ## 4. Check without modifying infrastructure
64
+ ### Check DNS
103
65
 
104
- From either current Worker template, run:
66
+ From either Worker template:
105
67
 
106
68
  ```sh
107
69
  npm run check:subdomains -- --base in.example.com --labels acme,globex
108
70
  ```
109
71
 
110
- This requires only Node and DNS access, not a Cloudflare API key. It resolves MX
111
- for your chosen labels plus two fresh random labels. DNS lookups have bounded
112
- timeouts. JSON output distinguishes observed Cloudflare MX, other/missing MX, and
113
- lookup uncertainty. Exit zero means only that all queried names resolved to
114
- Cloudflare MX; `delivery_verified` remains false. Compare returned targets with
115
- your account's assigned targets. Existing explicit records can explain differences.
116
- The command ships in the repository templates; gem 0.3.0's already-published
117
- template predates it, so refresh your template copy to use it.
72
+ This checks your chosen labels and two fresh labels without changing DNS.
73
+ It needs Node and DNS access, with no API key. A passing result confirms MX
74
+ answers; send a real email to verify delivery.
118
75
 
119
- For provider rule inspection, the gem also offers:
76
+ For configured Worker rules, use the
77
+ [Rails routing diagnostic](https://github.com/cole-robertson/cloudflare-email/blob/main/docs/routing-diagnostics.md).
78
+ That check inspects exact DNS records, so inherited wildcard MX may appear missing.
120
79
 
121
- ```sh
122
- bin/rails cloudflare:email:check_route \
123
- ADDRESS=invoices@acme.in.example.com \
124
- WORKER_NAME=cloudflare-email-ingress \
125
- ACCOUNT_ID=your-cloudflare-account-id
126
- ```
80
+ ## 4. Create the mailbox in Rails
127
81
 
128
- See [diagnostic credentials and limits](https://github.com/cole-robertson/cloudflare-email/blob/main/docs/routing-diagnostics.md).
129
- That diagnostic inspects exact-domain configured records; it does not resolve
130
- wildcard DNS inheritance. A working inherited-MX setup can therefore report
131
- missing exact records. Keep public DNS, provider rule inspection, and actual
132
- delivery evidence separate; do not force every organization through Cloudflare
133
- onboarding merely to make that diagnostic green.
134
-
135
- ## 5. Prove the dynamic path before adopting it
136
-
137
- Use two new organization labels that have not been added individually in the
138
- Cloudflare dashboard:
139
-
140
- 1. Check their public MX answers with the command above.
141
- 2. Through trusted provisioning code, register both **exact** domains in Rails,
142
- each against the correct stable organization key. Create a test mailbox for
143
- each. Record your independently checked configuration evidence and activate
144
- those test addresses so the gem can receive the verification messages.
145
- 3. Send real email to both addresses from an external mailbox. Confirm the
146
- Worker receives each, Rails retains the original message, and each appears
147
- only in the correct organization's inbox. Record the message IDs and result.
148
- 4. Repeat with a newly generated label without changing Cloudflare. If delivery
149
- fails before the Worker, resolve the Cloudflare acceptance/onboarding issue;
150
- changing Rails cannot fix a provider SMTP rejection.
151
- 5. Rehearse Rails being unavailable, then verify retained R2 mail drains after
152
- recovery without duplicate app records. Follow the
153
- [durable recovery guide](https://github.com/cole-robertson/cloudflare-email/blob/main/templates/worker/docs/durable-inbound.md).
154
-
155
- These are deliberate test addresses; no real-email test is sent by the DNS checker.
156
- Successful tests establish evidence for your receiving namespace, not a guarantee
157
- that every future DNS change or provider policy will preserve it.
158
-
159
- ## 6. Create organizations in Rails
160
-
161
- Once your namespace policy is verified, use the same gem APIs on organization
162
- creation. For example, from trusted app code after authorizing the organization:
82
+ After verifying the receiving route, run this from trusted application setup code:
163
83
 
164
84
  ```ruby
165
85
  registry = Cloudflare::Email::Mailboxes
166
- organization_key = "organization-123" # Trusted stable app identity, not request input.
167
86
  domain = registry.register_domain(
168
87
  domain: "acme.in.example.com",
169
- tenant_key: organization_key,
170
- account_id: Cloudflare::Email::Credentials.account_id
88
+ tenant_key: "organization-123"
171
89
  )
172
- registry.activate_domain!(domain.id, evidence: verified_namespace_evidence)
173
- registry.for_tenant(organization_key) do |inboxes|
174
- mailbox = inboxes.create(name: "Invoices", address: "invoices@acme.in.example.com",
175
- owner_ref: "Organization:123")
176
- inboxes.activate_address!(inboxes.addresses(mailbox.id).first.id,
177
- evidence: verified_namespace_evidence)
90
+ registry.activate_domain!(domain.id, evidence: "Receiving route verified")
91
+
92
+ registry.for_tenant("organization-123") do |inboxes|
93
+ mailbox = inboxes.create(
94
+ name: "Invoices",
95
+ address: "invoices@acme.in.example.com",
96
+ owner_ref: "Organization:123"
97
+ )
98
+ inboxes.activate_address!(mailbox.addresses.first.id,
99
+ evidence: "Receiving route verified")
178
100
  end
179
101
  ```
180
102
 
181
- `verified_namespace_evidence` is your stored operator evidence from the setup and
182
- delivery checks, not a hard-coded claim of success. Make provisioning idempotent
183
- in your app, validate/reserve slugs, and resolve organization identity from trusted
184
- records. The gem stores exact domain registrations; it does not accept a `*`
185
- domain registration or silently authorize an arbitrary recipient from a web form.
186
- The management engine must obtain allowed domains from your host adapter. The
187
- hello-world starter intentionally stays single-domain; this recipe is the next
188
- step for an organization-aware app.
189
-
190
- A **Cloudflare routing catch-all** gets mail to the Worker. The gem's optional
191
- **mailbox catch-all** decides whether unknown local parts enter a mailbox. They
192
- are separate. Without the latter, unknown/suspended recipients return non-2xx and
193
- remain in durable pending storage until resolved. Monitor that backlog.
194
-
195
- Receiving on dynamic domains does not authorize sending from them. Cloudflare
196
- [Email Sending onboarding](https://developers.cloudflare.com/email-service/configuration/subdomains/#add-a-subdomain-to-email-sending)
197
- is separate; keep a verified shared sending domain unless you have explicitly
198
- onboarded the customer sending domain.
103
+ Use your application's stable organization identity and validate customer slugs.
104
+ Activation records the verification you performed; it does not configure DNS.
105
+ Registration takes exact domains, not `*` patterns.
106
+
107
+ Once wildcard receiving is verified, create new customer domains and mailboxes
108
+ through these same APIs. Send a test message and confirm it appears only in the
109
+ intended inbox. Monitor pending mail so unregistered or suspended destinations
110
+ can be resolved.
111
+
112
+ ## Sending from customer addresses
113
+
114
+ Receiving and sending are separate. Cloudflare requires
115
+ [verification of each sending domain](https://developers.cloudflare.com/email-service/configuration/subdomains/#add-a-subdomain-to-email-sending).
116
+ A simple setup uses a shared verified From address and a customer-specific
117
+ Reply-To address. To send through a managed mailbox, follow the
118
+ [mailbox sending setup](https://github.com/cole-robertson/cloudflare-email/blob/main/docs/mailboxes.md#send-from-the-mailbox).
@@ -5,12 +5,6 @@ The bundled Worker uses R2 storage, Queue delivery and scheduled recovery by
5
5
  default. No enable flag, Rails model, database, or tenancy change is needed.
6
6
  Missing infrastructure fails visibly; it never silently downgrades to direct delivery.
7
7
 
8
- Upgrade the Rails gem before deploying this Worker. New messages without a parseable
9
- Message-ID use a deterministic fallback for deduplication across Rails hosts.
10
- Older records created before this change use ActionMailbox's hostname-dependent
11
- fallback; replaying those historical messages after upgrading can create a new
12
- record. Reconcile any existing archive backlog before replaying it automatically.
13
-
14
8
  ## Set it up
15
9
 
16
10
  Create a private R2 bucket and a Queue for each environment:
@@ -23,8 +17,7 @@ npx wrangler queues create cloudflare-email-inbound-production
23
17
  The bundled `wrangler.toml` already configures these resources for each environment.
24
18
  Customize their names if needed. Configure **all three** pieces: the `INBOUND_EMAIL_STORE` R2
25
19
  binding, `INBOUND_EMAIL_QUEUE` producer and consumer, and the once-per-minute
26
- scheduled trigger. Keep `RAILS_INGRESS_URL` and `INGRESS_SECRET` configured as
27
- before. Deploy with `npm run deploy -- --env production`.
20
+ scheduled trigger. Keep `RAILS_INGRESS_URL` and `INGRESS_SECRET` configured. Deploy with `npm run deploy -- --env production`.
28
21
 
29
22
  Use separate buckets, queues and secrets for staging and production. Do not add
30
23
  an R2 lifecycle rule that expires `cloudflare-email/pending/` objects. Pending
@@ -38,7 +31,7 @@ binding, and minute schedule before changing code or secrets. This check does no
38
31
  validate the bucket lifecycle or queue consumer: verify those in your infrastructure
39
32
  configuration and live drill. The Ruby task does not provision resources.
40
33
 
41
- ## Direct fallback and upgrades
34
+ ## Direct fallback
42
35
 
43
36
  Set `INBOUND_DELIVERY_MODE = "direct"` in the environment's Wrangler vars only when
44
37
  you deliberately want a single HTTP handoff. With the Ruby task, pass
@@ -49,15 +42,10 @@ recovery continue draining previously retained mail. To return to durable delive
49
42
  remove the Wrangler override or pass `INBOUND_DELIVERY_MODE=durable` to the Ruby task.
50
43
  Unknown values fail rather than choosing a transport implicitly.
51
44
 
52
- The old `DURABLE_INBOUND_ENABLED` rollout flag has been removed. Existing direct
53
- installations must provision the resources before upgrading the Worker, or explicitly
54
- select `INBOUND_DELIVERY_MODE=direct` before deploying. Upgrading the Ruby gem alone
55
- does not redeploy a Worker or change live routing.
56
-
57
45
  ## What happens to a message
58
46
 
59
47
  1. The Worker validates the envelope and actual MIME size. Invalid or oversized
60
- mail is permanently rejected, as in direct mode.
48
+ mail is permanently rejected.
61
49
  2. It writes one R2 object containing the exact raw bytes, SMTP envelope, original
62
50
  receive time, and optional provider metadata. It waits for the write before
63
51
  returning from the Email Worker handler.
@@ -129,7 +117,7 @@ callback is not canceled. Set `archiveTimeoutMs` to an integer from 1 to 120,000
129
117
  milliseconds to fit your host's runtime budget. Treat bytes as read-only and never parse the private
130
118
  pending frame to build an archive. The primary R2 write is always mandatory.
131
119
  Trusted provider metadata is captured once at receipt, never recomputed on replay.
132
- The existing `relayEmail` archive callback is best effort and does not enable this
120
+ The `relayEmail` archive callback is best effort and does not enable this
133
121
  durable protocol automatically.
134
122
 
135
123
  ## Guarantee boundary
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudflare-email
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cole
@@ -156,7 +156,6 @@ files:
156
156
  - app/controllers/cloudflare/email/management/mailboxes_controller.rb
157
157
  - app/controllers/cloudflare/email/management/styles_controller.rb
158
158
  - docs/activerecord-tenanted.md
159
- - docs/architecture.md
160
159
  - docs/custom-ingress.md
161
160
  - docs/delivery-events.md
162
161
  - docs/features.md
@@ -164,25 +163,11 @@ files:
164
163
  - docs/mailboxes.md
165
164
  - docs/management-engine.md
166
165
  - docs/outbox.md
166
+ - docs/reference.md
167
167
  - docs/routing-deliveries.md
168
168
  - docs/routing-diagnostics.md
169
- - docs/tenant-mailboxes-plan.md
170
169
  - docs/thread-correlation.md
171
170
  - docs/troubleshooting.md
172
- - docs/upgrading-0.2.md
173
- - docs/upgrading-0.3.md
174
- - docs/verification/2026-09-10-followup.md
175
- - docs/verification/2026-09-10-install-upgrade.md
176
- - docs/verification/2026-09-10-live.md
177
- - docs/verification/2026-09-10.md
178
- - docs/verification/2026-09-11-custom-ingress.md
179
- - docs/verification/2026-09-11-extraction.md
180
- - docs/verification/2026-09-11-outbound-ledger.md
181
- - docs/verification/2026-09-11-security.md
182
- - docs/verification/2026-09-11-tenant-mailboxes.md
183
- - docs/verification/2026-09-12-destination-context.md
184
- - docs/verification/2026-09-13-action-mailbox-core.md
185
- - docs/verification/2026-09-13-durable-inbound-and-routing.md
186
171
  - examples/plain_ruby.rb
187
172
  - lib/cloudflare-email.rb
188
173
  - lib/cloudflare/email/active_record.rb
data/docs/architecture.md DELETED
@@ -1,105 +0,0 @@
1
- # Gem and inbox responsibilities
2
-
3
- The Cloudflare gem supplies email transport and reusable delivery infrastructure.
4
- The inbox application supplies a product built on that infrastructure. Plain Ruby
5
- users should not need Rails or a database. Rails users should be able to opt into
6
- durable infrastructure without copying the reference inbox's models and services.
7
-
8
- ## Implemented in this extraction
9
-
10
- | Gem capability | Application responsibility |
11
- | --- | --- |
12
- | Optional mailbox/address directory and tenant connection adapter | Authorize domain ownership, provision tenant databases and grant user access |
13
- | Mailbox memberships, read/archive state and retained raw mail | Inbox UI, retention schedule and explicit purge policy |
14
- | Shared event intake, tenant correlation and tenant-aware jobs | Durable queue workers, scheduled recovery and schema rollout to every tenant |
15
- | Structured/raw sending and ActionMailer transport | Compose UI, recipients and send authorization |
16
- | `Response#accepted?`, plus individual recipient outcomes | Handle partial acceptance without resending accepted recipients |
17
- | Authenticated ingress (default v2; opt-in v3 [metadata](custom-ingress.md)) and context-scoped deduplication | Map trusted recipient to an authorized mailbox; apply sender and document policy |
18
- | `MessageId.normalize` and returned provider IDs | Store conversation membership and scope reply lookups |
19
- | Queue decoding, validation and ACK | Queue/subscription setup and recurring execution |
20
- | Optional ActiveRecord event receipts, deduplication and indexed replay | Configure queue polling and receipt retention |
21
- | Immutable outbox snapshots, send claims, per-recipient outcomes | Authorize sending and choose a stable operation key |
22
- | `DeliveryEvents` account/message/recipient correlation and ordering | Transactional callback to update product records |
23
- | Send/replay jobs, recovery tasks and audited reconciliation | Authorize operators and provide evidence of provider outcome |
24
-
25
- The ActiveRecord adapter is explicit opt-in. Its generator installs a receipt table
26
- and initializer; the default plain Ruby client does not load ActiveRecord. Its
27
- handler applies application writes on the same database connection as the receipt.
28
- It does not wrap HTTP calls or other external effects in a database transaction.
29
-
30
- Version 0.2 is a preproduction break: v1 ingress and signed Message-ID tokens are
31
- removed. Cloudflare's documented queue encodings and recipient-response forms are
32
- provider contracts, so accepting them is not obsolete application compatibility.
33
- Historical verification reports describe their dated runs, not the current API.
34
-
35
- ## Implemented: durable outbound delivery
36
-
37
- The inbox now uses the gem's ledger tables and orchestration. Its model subclasses
38
- add application message associations and display snapshots; they do not maintain
39
- a second sending ledger. See the [outbox guide](outbox.md) for installation and API.
40
-
41
- 1. Claim an application-supplied operation key scoped to an account using a unique
42
- index before sending; repeated or concurrent calls cannot silently resend.
43
- 2. Persist immutable rendered MIME and envelope recipients before the network
44
- request. Track each recipient separately, including partial acceptance.
45
- 3. Separate preparation failures from ambiguous outcomes. A timeout, process death
46
- or persistence failure after acceptance keeps the operation blocked. An elapsed
47
- timeout alone never proves the message was not sent.
48
- 4. Persist validated provider IDs and recipient acceptance outcomes separately
49
- from later lifecycle events. Replay correlates account, message and recipient;
50
- conflicting IDs remain uncertain. The network and database are not atomic.
51
- 5. Expose audited reconciliation with caller-supplied actor/reason and evidence.
52
- The application authorizes the operator; the gem enforces legal transitions.
53
- 6. Provide jobs/tasks and notifications that preserve the same claim on retries.
54
- Include migration/import support for the reference inbox's existing attempts.
55
-
56
- The suite exercises concurrent senders, actual process termination, acceptance
57
- followed by persistence failure, partial recipients, event-before-response,
58
- out-of-order events, callbacks that roll back, and recovery without a duplicate
59
- send. PostgreSQL and SQLite run separate concurrency checks. Inbox HTTP and browser
60
- workflows use the shared implementation, with controlled local provider responses.
61
-
62
- Historic attempts did not preserve rendered MIME. Their import retains available
63
- bodies, IDs, outcomes and operator evidence and marks reconstructed snapshots.
64
- Unknown historical outcomes remain blocked. No migration claims to recreate the
65
- exact bytes of a previously sent message.
66
-
67
- ## Operational completion
68
-
69
- The reference inbox supplies a functional mailbox product over these APIs. Before
70
- production use, configure sending DNS and receiving routes, queue subscriptions
71
- and dead-letter handling, durable Rails jobs, receipt/MIME retention and backups,
72
- and monitoring for prepared/sending/unknown/partial operations. Exercise live
73
- external mailbox delivery and recovery with the deployed revision. Installation
74
- does not automatically provision domains or deploy infrastructure.
75
-
76
- Current limitations are deliberate and visible: no provider exactly-once API,
77
- no automatic resend of uncertain or rejected operations, no multi-provider-ID
78
- operation (conflicting IDs require review). Direct inbound forwarding can reject
79
- mail during a Rails outage. The default [durable Worker path](../templates/worker/README.md#durable-inbound-delivery)
80
- commits raw mail and stable envelope context to R2 before returning successfully.
81
- Queue handoffs and a scheduled recovery pass retry pending mail; a Rails 2xx
82
- allows removal from the pending store. Queue expiration does not remove R2 mail.
83
- This boundary requires durable Rails persistence before 2xx, idempotent replay,
84
- private R2 storage without expiration of pending objects, and operational
85
- monitoring. It cannot guarantee acceptance when Cloudflare itself cannot store
86
- the incoming message.
87
- A prepared operation can be dispatched using its existing identity after a job
88
- enqueue failure; sending/unknown operations require evidence-based reconciliation.
89
- Notifications indicate method execution and may run inside an outer transaction;
90
- the durable database record is authoritative.
91
-
92
- ## Keep in the inbox product
93
-
94
- Users, tenancy, mailbox ownership, conversations, folders, search and UI remain in
95
- the inbox. AI context, review flags, approval thresholds, auto-send policy and
96
- operator permissions also stay there. Suppression/unsubscribe requirements depend
97
- on message purpose and application policy; expose provider outcomes, but do not
98
- silently impose a marketing subscription model on transactional mail.
99
-
100
- The optional [mailbox module](mailboxes.md) now packages mailbox persistence and
101
- address management over the existing transport/receipt APIs. It includes a
102
- [tenant adapter](activerecord-tenanted.md), without adding a tenant library to
103
- the plain Ruby client. The optional [management engine](management-engine.md)
104
- consumes these same APIs with host-supplied authentication and authorization.
105
- Full inbox products can continue using them through their own frontends.