cloudflare-email 0.1.0 → 0.2.0

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -1
  3. data/README.md +265 -388
  4. data/SECURITY.md +51 -0
  5. data/app/controllers/cloudflare/email/ingress_controller.rb +62 -6
  6. data/docs/activerecord-tenanted.md +135 -0
  7. data/docs/architecture.md +95 -0
  8. data/docs/delivery-events.md +177 -0
  9. data/docs/features.md +107 -0
  10. data/docs/getting-started.md +298 -0
  11. data/docs/mailboxes.md +299 -0
  12. data/docs/outbox.md +175 -0
  13. data/docs/tenant-mailboxes-plan.md +149 -0
  14. data/docs/thread-correlation.md +27 -0
  15. data/docs/troubleshooting.md +103 -0
  16. data/docs/upgrading-0.2.md +96 -0
  17. data/docs/verification/2026-09-10-followup.md +49 -0
  18. data/docs/verification/2026-09-10-install-upgrade.md +86 -0
  19. data/docs/verification/2026-09-10-live.md +102 -0
  20. data/docs/verification/2026-09-10.md +175 -0
  21. data/docs/verification/2026-09-11-extraction.md +32 -0
  22. data/docs/verification/2026-09-11-outbound-ledger.md +60 -0
  23. data/docs/verification/2026-09-11-security.md +76 -0
  24. data/docs/verification/2026-09-11-tenant-mailboxes.md +75 -0
  25. data/examples/plain_ruby.rb +24 -0
  26. data/lib/cloudflare/email/active_record/base.rb +48 -0
  27. data/lib/cloudflare/email/active_record/delivery_events.rb +72 -0
  28. data/lib/cloudflare/email/active_record/event_inbox.rb +77 -0
  29. data/lib/cloudflare/email/active_record/event_receipt.rb +17 -0
  30. data/lib/cloudflare/email/active_record/mail_snapshot.rb +24 -0
  31. data/lib/cloudflare/email/active_record/outbound_delivery.rb +22 -0
  32. data/lib/cloudflare/email/active_record/outbound_recipient.rb +13 -0
  33. data/lib/cloudflare/email/active_record/outbound_reconciliation.rb +15 -0
  34. data/lib/cloudflare/email/active_record/outbox.rb +219 -0
  35. data/lib/cloudflare/email/active_record/outbox_notifications.rb +34 -0
  36. data/lib/cloudflare/email/active_record.rb +5 -0
  37. data/lib/cloudflare/email/client.rb +51 -11
  38. data/lib/cloudflare/email/consume_events_task.rb +22 -0
  39. data/lib/cloudflare/email/delivery_event.rb +70 -0
  40. data/lib/cloudflare/email/delivery_method.rb +3 -0
  41. data/lib/cloudflare/email/deploy_worker_task.rb +3 -0
  42. data/lib/cloudflare/email/dev_ingress_guard.rb +27 -0
  43. data/lib/cloudflare/email/dev_tunnel.rb +31 -5
  44. data/lib/cloudflare/email/doctor.rb +16 -38
  45. data/lib/cloudflare/email/endpoint.rb +24 -0
  46. data/lib/cloudflare/email/engine.rb +28 -0
  47. data/lib/cloudflare/email/envelope.rb +58 -0
  48. data/lib/cloudflare/email/event_consumer.rb +86 -0
  49. data/lib/cloudflare/email/mailboxes/configuration.rb +58 -0
  50. data/lib/cloudflare/email/mailboxes/events.rb +112 -0
  51. data/lib/cloudflare/email/mailboxes/inbound_retention.rb +14 -0
  52. data/lib/cloudflare/email/mailboxes/jobs.rb +34 -0
  53. data/lib/cloudflare/email/mailboxes/models.rb +148 -0
  54. data/lib/cloudflare/email/mailboxes/provider_correlation.rb +11 -0
  55. data/lib/cloudflare/email/mailboxes/service.rb +288 -0
  56. data/lib/cloudflare/email/mailboxes/shared_event_receipt.rb +14 -0
  57. data/lib/cloudflare/email/mailboxes.rb +11 -0
  58. data/lib/cloudflare/email/message_id.rb +12 -0
  59. data/lib/cloudflare/email/provision_catchall_task.rb +4 -4
  60. data/lib/cloudflare/email/provision_route_task.rb +2 -1
  61. data/lib/cloudflare/email/replay_events_job.rb +18 -0
  62. data/lib/cloudflare/email/response.rb +21 -3
  63. data/lib/cloudflare/email/routing_provisioner.rb +71 -45
  64. data/lib/cloudflare/email/send_job.rb +41 -0
  65. data/lib/cloudflare/email/send_test.rb +5 -29
  66. data/lib/cloudflare/email/signing.rb +2 -2
  67. data/lib/cloudflare/email/tenancy.rb +78 -0
  68. data/lib/cloudflare/email/tenant_job_context.rb +90 -0
  69. data/lib/cloudflare/email/verification.rb +30 -8
  70. data/lib/cloudflare/email/version.rb +1 -1
  71. data/lib/cloudflare/email/worker_deployer.rb +5 -3
  72. data/lib/cloudflare-email.rb +4 -1
  73. data/lib/generators/cloudflare/email/install_generator.rb +48 -33
  74. data/lib/generators/cloudflare/email/mailboxes/mailboxes_generator.rb +55 -0
  75. data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_mailboxes.rb +54 -0
  76. data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_receiving_domains.rb +16 -0
  77. data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_shared_events.rb +35 -0
  78. data/lib/generators/cloudflare/email/outbox/outbox_generator.rb +28 -0
  79. data/lib/generators/cloudflare/email/outbox/templates/create_cloudflare_email_outbox.rb +48 -0
  80. data/lib/generators/cloudflare/email/templates/initializer.rb +3 -3
  81. data/lib/generators/cloudflare/email/templates/main_mailbox.rb +2 -7
  82. data/lib/generators/cloudflare/email/tracking/templates/create_cloudflare_email_event_receipts.rb +22 -0
  83. data/lib/generators/cloudflare/email/tracking/tracking_generator.rb +26 -0
  84. data/lib/tasks/cloudflare_email.rake +41 -4
  85. data/templates/worker/README.md +61 -11
  86. data/templates/worker/package-lock.json +2627 -0
  87. data/templates/worker/package.json +8 -5
  88. data/templates/worker/scripts/wrangler.mjs +16 -0
  89. data/templates/worker/src/index.js +83 -4
  90. data/templates/worker/test/index.test.ts +117 -5
  91. data/templates/worker/wrangler.toml +13 -4
  92. metadata +107 -6
  93. data/lib/cloudflare/email/secure_message_id.rb +0 -89
data/docs/features.md ADDED
@@ -0,0 +1,107 @@
1
+ # What cloudflare-email does
2
+
3
+ cloudflare-email connects your Ruby or Rails application to Cloudflare's email
4
+ services. You can use just the sending client, add incoming mail, or build a
5
+ mailbox on top of the optional database-backed delivery tools.
6
+
7
+ Start with [Getting started](getting-started.md) for working examples. These
8
+ features are available in **0.2.0**. Database multi-tenancy is **off by default**:
9
+ the optional mailbox module works in one database unless you explicitly configure
10
+ a tenant connection adapter. A mailbox tenant key alone does not switch databases.
11
+
12
+ ## Choose the pieces you need
13
+
14
+ | You want to… | Use | What you get |
15
+ | --- | --- | --- |
16
+ | Create managed inboxes in code | Optional `Mailboxes` module | Named mailboxes, aliases, ownership references, read/archive state and retained raw mail |
17
+ | Isolate organizations in separate databases | Optional `Tenancy` adapter | Configurable ActiveRecord base, tenant-aware jobs and shared-to-tenant event replay; tested with SQLite and `activerecord-tenanted` |
18
+ | Send from an ordinary Ruby program | `Cloudflare::Email::Client` | Structured messages or complete raw MIME; no Rails/database required |
19
+ | Send existing Rails mailers | ActionMailer delivery method | Your mailer templates, attachments, multipart bodies, cc/bcc and reply headers sent through Cloudflare |
20
+ | Receive email in Rails | Email Worker + ActionMailbox | Unchanged raw MIME, attachments, authenticated SMTP envelope metadata and duplicate handling |
21
+ | Develop against real incoming email locally | `cloudflare:email:dev` | A temporary tunnel restricted to your email ingress |
22
+ | Save an email before sending it | Optional ActiveRecord outbox | Immutable message snapshot, saved recipients, a send claim, and per-recipient results |
23
+ | Avoid accidentally resending after a crash or timeout | `SendJob` + outbox | Jobs use the saved operation identity; uncertain attempts stay blocked for review |
24
+ | Track delivery, bounce or complaint updates | `EventConsumer` + Cloudflare Queue | Validated lifecycle events, account/domain checks and acknowledgement after successful handling |
25
+ | Avoid building your own event ledger | Optional tracking tables | Durable receipts, duplicate/conflict detection, unmatched-event storage and replay |
26
+ | Connect events to outbox recipients | `DeliveryEvents` | Account/message/recipient matching, event ordering and callbacks for your app's records |
27
+ | Investigate an uncertain send | Recovery tasks + `Outbox.reconcile` | Operator-supplied evidence and an audit trail; no automatic resend based on elapsed time |
28
+ | Match replies to earlier messages | Provider message IDs + `MessageId.normalize` | A consistent lookup key for your app's conversations |
29
+ | Observe email processing | ActiveSupport notifications + `doctor` | Send/ingress/event/outbox instrumentation and configuration diagnostics |
30
+ | Deploy receiving infrastructure | Ruby deployer and routing tasks | Environment-specific Workers, ingress secrets, address routes and DNS preflight checks |
31
+
32
+ ## SQLite works
33
+
34
+ The optional receipt and outbox tables work with **SQLite**. Keep your existing
35
+ Rails SQLite database; PostgreSQL is optional, and both adapters have concurrency
36
+ coverage. The gem uses ActiveRecord and does not install a separate database
37
+ service. You still choose a durable Rails job backend and run its workers.
38
+
39
+ Database locking conflicts can require a job retry. A retry uses the existing
40
+ outbox identity; it does not clear an uncertain send claim. Keep application
41
+ writes and gem callbacks on the same database connection when they must commit
42
+ together. See [outbox setup](outbox.md).
43
+
44
+ ## Understand the different kinds of success
45
+
46
+ | Result | What it tells you |
47
+ | --- | --- |
48
+ | Successful API request | Cloudflare returned a successful response; inspect recipient outcomes too |
49
+ | `response.accepted?` | There is evidence of acceptance for at least part of the send |
50
+ | Outbox `accepted` | Every recipient has acceptance evidence |
51
+ | Outbox `partial` | Some recipients have acceptance evidence; the others need individual attention |
52
+ | Delivery event `delivered` | The recipient's server accepted the email; this is not a read receipt |
53
+ | Ingress HTTP 200 | Rails stored the message or recognized a duplicate; mailbox processing happens separately |
54
+
55
+ Sending, receiving and delivery tracking are separate Cloudflare setups. A
56
+ working sending domain does not automatically enable incoming routes or queue
57
+ subscriptions. The [getting-started guide](getting-started.md) walks through each.
58
+
59
+ ## Protections included
60
+
61
+ Incoming requests use v2 HMAC signatures covering the timestamp, SMTP sender,
62
+ SMTP recipient and raw message. Rails checks a five-minute timestamp window.
63
+ Identical MIME retried for the same recipient is deduplicated; delivery to a
64
+ different recipient is stored separately.
65
+
66
+ Rails and the Worker default to a **25 MiB raw incoming message limit**. Both
67
+ support a positive `MAX_EMAIL_BYTES` override. Remote endpoints require HTTPS,
68
+ the Worker refuses redirects and limits its Rails request to 15 seconds, and
69
+ the development tunnel only routes ingress POSTs. Client inspection and default
70
+ retry/mailbox logging omit sensitive details.
71
+
72
+ The outbox preserves the rendered message and acceptance evidence, blocks
73
+ ambiguous resend attempts, and records reconciliation decisions. Delivery-event
74
+ validation rejects malformed schema fields; original receipt identity and
75
+ payload are read-only through normal ActiveRecord updates. These safeguards do
76
+ not make database administrators untrusted or provide exactly-once delivery.
77
+
78
+ See [managed mailbox setup](mailboxes.md) for the application-facing API and
79
+ [activerecord-tenanted](activerecord-tenanted.md) for separate SQLite databases.
80
+
81
+ ## What belongs in your mailbox application
82
+
83
+ The gem supplies email infrastructure and optional managed mailbox records.
84
+ Your app supplies users, mailbox permissions, conversation records, custom folders, search, compose screens,
85
+ drafts and any AI review or approval workflow. It also decides unsubscribe and
86
+ recipient eligibility policy. The gem does not supply an inbox UI or AI agent.
87
+
88
+ An authenticated SMTP envelope tells your app which address Cloudflare received
89
+ the message for. It does not prove the human sender's identity. Reply correlation
90
+ also does not authorize access to a conversation. Apply your app's permissions
91
+ before displaying mail or sending a reply.
92
+
93
+ You configure Cloudflare domains/DNS, queue subscriptions, durable jobs,
94
+ monitoring, storage protection and retention. Inbound mail is not durably
95
+ buffered by the Worker during a Rails outage. See [architecture](architecture.md)
96
+ for the full boundary and [security guidance](../SECURITY.md) for deployment.
97
+
98
+ ## Where to go next
99
+
100
+ - [Build your first integration](getting-started.md)
101
+ - [Use plain Ruby, attachments or raw MIME](../README.md#plain-ruby)
102
+ - [Configure client retries and errors](../README.md#retry-and-configuration)
103
+ - [Set up event polling and durable receipts](delivery-events.md)
104
+ - [Recover saved outbound operations](outbox.md)
105
+ - [Correlate replies](thread-correlation.md)
106
+ - [Use Cloudflare SMTP instead of the HTTP delivery method](../README.md#smtp-alternative)
107
+ - [Troubleshoot your setup](troubleshooting.md)
@@ -0,0 +1,298 @@
1
+ # Get email working in your Rails app
2
+
3
+ This guide takes you from your first outgoing email to receiving replies and
4
+ tracking deliveries. Start with sending and add the other pieces as needed.
5
+ For a mailbox app, follow all four parts. **SQLite is supported throughout.**
6
+
7
+ You will need an existing Rails app, a Cloudflare account, and a domain you can
8
+ configure in Cloudflare. Use Ruby 3.2+ and a patched supported Rails release:
9
+ tested floors are 7.2.3.2, 8.0.5.1 and 8.1.3.1. Rails 8 requires Ruby 3.3+.
10
+ Ruby 4 is tested with Rails 8.1. Plain Ruby users can use the
11
+ [standalone client](../README.md#plain-ruby) instead.
12
+
13
+ Example addresses below use `mail.example.com` for sending and
14
+ `in.example.com` for receiving. Replace them with subdomains you own. They do
15
+ not need to be the same domain as your Rails application's web address.
16
+
17
+ ## 1. Send your first email
18
+
19
+ ### Install the current code
20
+
21
+ Add version 0.2 to your app's `Gemfile`. If upgrading from 0.1, read the
22
+ [upgrade guide](upgrading-0.2.md) before changing an existing Worker deployment:
23
+
24
+ ```ruby
25
+ gem "cloudflare-email", "~> 0.2.0"
26
+ ```
27
+
28
+ ```sh
29
+ bundle install
30
+ bin/rails generate cloudflare:email:install --no-inbound
31
+ ```
32
+
33
+ The generator creates an initializer that chooses `:cloudflare` as your
34
+ ActionMailer delivery method. The `--no-inbound` option keeps this first step
35
+ focused on sending; you can add receiving below.
36
+
37
+ ### Connect your sending domain
38
+
39
+ In Cloudflare, open **Compute → Email Service → Email Sending** and onboard
40
+ your sending domain. Complete Cloudflare's DNS instructions and wait for domain
41
+ verification. A dedicated subdomain helps keep an existing mail provider's apex
42
+ configuration separate. Do not replace existing apex mail records casually.
43
+
44
+ Create an account-scoped token with permission to send email. Add these variables
45
+ to the environment where Rails runs, or use the equivalent Rails credentials:
46
+
47
+ ```sh
48
+ export CLOUDFLARE_ACCOUNT_ID=your-account-id
49
+ export CLOUDFLARE_API_TOKEN=your-email-sending-token
50
+ ```
51
+
52
+ Rails credentials use `cloudflare.account_id` and `cloudflare.api_token`.
53
+ Nonempty Rails credentials take precedence over environment variables. Keep
54
+ real tokens out of committed files. Restart Rails after changing configuration.
55
+
56
+ ### Check the setup and send
57
+
58
+ ```sh
59
+ bin/rails cloudflare:email:doctor
60
+ FROM=hello@mail.example.com TO=you@example.net bin/rails cloudflare:email:send_test
61
+ ```
62
+
63
+ Use a destination inbox you control. `doctor` checks configuration and available
64
+ read access; the second command actually sends mail. Confirm that it arrives,
65
+ including checking spam. A diagnostics success alone does not prove delivery.
66
+
67
+ ### Use your regular mailers
68
+
69
+ ```ruby
70
+ # app/mailers/hello_mailer.rb
71
+ class HelloMailer < ApplicationMailer
72
+ def hello(address)
73
+ mail(from: "hello@mail.example.com", to: address, subject: "Hello from Rails") do |format|
74
+ format.text { render plain: "Your email integration is working." }
75
+ end
76
+ end
77
+ end
78
+ ```
79
+
80
+ From `bin/rails console`:
81
+
82
+ ```ruby
83
+ HelloMailer.hello("you@example.net").deliver_now
84
+ ```
85
+
86
+ Your existing HTML templates, multipart messages, attachments and cc/bcc work
87
+ through ActionMailer. Use `deliver_later` with your app's job backend for ordinary
88
+ background delivery. For saved send history and protection against ambiguous
89
+ resends, use the outbox in part 3. Do not send the same message through both paths.
90
+
91
+ ## 2. Receive email and replies
92
+
93
+ Incoming email follows this path:
94
+
95
+ ```text
96
+ Sender → Cloudflare Email Routing → Email Worker → Rails ActionMailbox → your app
97
+ ```
98
+
99
+ The Worker is a small program that forwards the email to Rails and signs it so
100
+ Rails can verify the forwarding request. The installer supplies its code.
101
+
102
+ ### Install receiving support
103
+
104
+ ```sh
105
+ bin/rails generate cloudflare:email:install
106
+ bin/rails db:migrate
107
+ ```
108
+
109
+ Follow the interactive prompts to install ActionMailbox and the default mailbox.
110
+ If you already have custom mailbox routing or initializer changes, inspect the
111
+ generator's overwrite prompts before accepting. The generated default mailbox
112
+ only logs receipt; you will add your product's processing logic.
113
+
114
+ Save the generated shared secret as `cloudflare.ingress_secret` in Rails
115
+ credentials or `CLOUDFLARE_INGRESS_SECRET`. The deploy task copies that secret to
116
+ the Worker. Use a separate deployment token as `cloudflare.management_token` or
117
+ `CLOUDFLARE_MANAGEMENT_TOKEN`; [the permission table](../README.md#observability-and-permissions)
118
+ lists the scopes needed for Worker deployment and routing.
119
+
120
+ ### Connect the receiving address
121
+
122
+ In Cloudflare **Email Routing → your apex domain → Settings → Subdomains**, add
123
+ your receiving subdomain, such as `in.example.com`, and finish its DNS setup.
124
+ This is separate from verifying the sending domain.
125
+
126
+ Deploy Rails with ActionMailbox storage and workers configured. Then run these
127
+ commands in an environment with production configuration and deployment credentials:
128
+
129
+ ```sh
130
+ RAILS_ENV=production bin/rails cloudflare:email:deploy_worker URL=https://app.example.com/rails/action_mailbox/cloudflare/inbound_emails
131
+ RAILS_ENV=production bin/rails cloudflare:email:provision_route ADDRESS=support@in.example.com
132
+ ```
133
+
134
+ The first command deploys the Worker; the second routes the address to it.
135
+ Send a real test email to `support@in.example.com`, then check the ActionMailbox
136
+ record and processing job. A web request accepted by Rails means storage worked;
137
+ the mailbox job can still need attention.
138
+
139
+ ### Read the message in your mailbox
140
+
141
+ Inside the generated `MainMailbox#process`, these values are available:
142
+
143
+ ```ruby
144
+ envelope = Cloudflare::Email::Envelope.for(inbound_email)
145
+ receiving_address = envelope&.fetch("to")
146
+ subject = mail.subject
147
+ text = mail.text_part&.body&.decoded || (mail.body.decoded unless mail.multipart?)
148
+ attachments = mail.attachments
149
+ raw_message = inbound_email.raw_email.download
150
+ ```
151
+
152
+ Save or process those values using your application's models. For mailbox or
153
+ tenant selection, use `receiving_address`, which comes from the authenticated
154
+ SMTP envelope. MIME `To`/`Cc` headers may differ, especially for Bcc. The envelope
155
+ can be absent for records from another ingress; handle that case explicitly.
156
+ Treat message contents and attachments as untrusted input when displaying them.
157
+
158
+ To direct replies to this address, add `reply_to: "support@in.example.com"` to
159
+ your mailer's `mail(...)` call. For conversation matching, save the returned
160
+ provider message ID and compare reply headers within your application's mailbox
161
+ scope. The outbox saves this ID for you. See [thread correlation](thread-correlation.md).
162
+
163
+ ### Receive into a local Rails server
164
+
165
+ Install `cloudflared`, configure development credentials and a separate development
166
+ receiving route, then start Rails. In another terminal:
167
+
168
+ ```sh
169
+ bin/rails cloudflare:email:deploy_worker
170
+ bin/rails cloudflare:email:dev
171
+ ```
172
+
173
+ The first command creates the development Worker; the second points it at a
174
+ temporary tunnel to your local Rails server on port 3000. Keep the tunnel running
175
+ while testing and stop it with Ctrl-C. Set `PORT=...` if Rails uses another port.
176
+ The task checks that Rails has the ingress-only guard; restart Rails if asked.
177
+ Use development test data. The tunnel URL is temporary and does not provide an
178
+ offline mailbox when your local server is stopped.
179
+
180
+ ## 3. Add the durable outbox with SQLite
181
+
182
+ An outbox saves the exact message before sending it. Background jobs reference
183
+ that saved operation instead of re-rendering and resending mail on every retry.
184
+ This is useful for compose, replies, invoices and approved drafts.
185
+
186
+ If your Rails app already uses SQLite, keep its database configuration. Run:
187
+
188
+ ```sh
189
+ bin/rails generate cloudflare:email:tracking
190
+ bin/rails generate cloudflare:email:outbox
191
+ bin/rails db:migrate
192
+ ```
193
+
194
+ These add the receipt and delivery tables plus opt-in initializers. Skip a
195
+ generator whose migration you already installed. Configure a durable ActiveJob
196
+ backend and run a worker that processes the `mailers` queue; the gem does not
197
+ install the backend for you.
198
+
199
+ Try this from Rails console, outside an existing database transaction:
200
+
201
+ ```ruby
202
+ account_id = Cloudflare::Email::Credentials.account_id
203
+ operation = Cloudflare::Email::ActiveRecord::Outbox.prepare_mail(
204
+ account_id: account_id,
205
+ operation_key: "getting-started:hello:1",
206
+ mail: HelloMailer.hello("you@example.net").message,
207
+ )
208
+ Cloudflare::Email::SendJob.perform_later(account_id, operation.operation_key)
209
+ ```
210
+
211
+ The operation key identifies **one intended send** in your account. Save it with
212
+ your product record. For a genuinely new message use a new key; for a job retry
213
+ reuse the saved identity. Do not re-render a mailer and call `prepare_mail` as
214
+ your retry mechanism: generated MIME headers can change, causing a snapshot conflict.
215
+
216
+ In an application transaction, prepare the operation alongside your business
217
+ record, then enqueue only after the outer transaction commits. The detailed
218
+ [outbox guide](outbox.md#prepare-once-dispatch-after-commit) shows that pattern.
219
+
220
+ After the worker runs, inspect the result:
221
+
222
+ ```ruby
223
+ operation.reload.state
224
+ operation.provider_message_id
225
+ operation.outbound_recipients.pluck(:recipient, :acceptance_state, :state)
226
+ ```
227
+
228
+ `accepted` means all recipients have provider acceptance evidence; `partial`
229
+ means only some do. Neither promises that a human received or read the message.
230
+ `unknown` or a stuck `sending` operation needs investigation, not a new send key
231
+ as a shortcut. See [states and recovery](outbox.md#states-and-retries).
232
+
233
+ ## 4. Track delivery, bounces and complaints
234
+
235
+ Cloudflare sends later delivery updates through a Queue. You configure that
236
+ Queue and its Email Sending subscription once; your app polls it regularly.
237
+ Follow [the provisioning steps](delivery-events.md#provision-once), including an
238
+ HTTP pull consumer, retries and a dead-letter queue for repeatedly failing events.
239
+
240
+ Add a separate queue token and queue ID to your Rails environment:
241
+
242
+ ```sh
243
+ export CLOUDFLARE_QUEUES_TOKEN=your-queues-read-write-token
244
+ export CLOUDFLARE_EVENT_QUEUE_ID=your-queue-id
245
+ ```
246
+
247
+ With the outbox installed, configure this initializer:
248
+
249
+ ```ruby
250
+ # config/initializers/cloudflare_delivery_events.rb
251
+ require "cloudflare/email/active_record"
252
+
253
+ Rails.application.configure do
254
+ config.x.cloudflare_email.event_domains = ["mail.example.com"]
255
+ config.x.cloudflare_email.event_handler = ->(event) {
256
+ Cloudflare::Email::ActiveRecord::DeliveryEvents.record(event)
257
+ }
258
+ end
259
+ ```
260
+
261
+ Restart Rails and poll a batch:
262
+
263
+ ```sh
264
+ bin/rails cloudflare:email:consume_events
265
+ ```
266
+
267
+ This command processes **one batch and exits**. Schedule it repeatedly using
268
+ your application's scheduler. Also schedule receipt replay for recovery:
269
+
270
+ ```sh
271
+ bin/rails cloudflare:email:replay_events
272
+ ```
273
+
274
+ The gem saves receipts before acknowledgement, handles duplicates, and matches
275
+ updates to the account, provider message ID and recipient. If an event arrives
276
+ before the send result is saved, it stays available for replay. Recipient state
277
+ then reflects delivered, deferred, bounced, failed, rejected or complained events.
278
+ Your app still decides what those outcomes mean for its UI and future sending.
279
+
280
+ For custom product updates, use the [transactional callbacks](outbox.md#recover-dispatch-and-application-projections).
281
+ If you only want events and have your own delivery models, use the lower-level
282
+ [durable receipt adapter](delivery-events.md#durable-rails-receipts).
283
+
284
+ ## Before relying on it
285
+
286
+ Send to an external inbox you control, reply back, verify stored attachments,
287
+ and confirm a real queue event updates the intended recipient. Keep a durable
288
+ job worker running and schedule recovery of `prepared` operations that might
289
+ have committed before enqueueing. Monitor stuck operations and failed mailbox jobs.
290
+
291
+ Protect stored MIME and attachments, choose retention and backup policies, and
292
+ set proxy/server request limits. The default incoming raw message limit is
293
+ 25 MiB; set the same positive `MAX_EMAIL_BYTES` on Rails and the Worker if you
294
+ override it. Cloudflare has separate [outgoing size and recipient limits](https://developers.cloudflare.com/email-service/platform/limits/).
295
+
296
+ Use [troubleshooting](troubleshooting.md) when something does not arrive,
297
+ [the feature overview](features.md) to explore more, and
298
+ [the upgrade guide](upgrading-0.2.md) for an existing 0.1 installation.