cloudflare-email 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +149 -1
- data/README.md +287 -385
- data/SECURITY.md +51 -0
- data/app/controllers/cloudflare/email/ingress_controller.rb +33 -21
- data/app/controllers/cloudflare/email/management/mailboxes_controller.rb +173 -0
- data/app/controllers/cloudflare/email/management/styles_controller.rb +13 -0
- data/app/views/cloudflare/email/management/mailboxes/index.html.erb +65 -0
- data/app/views/cloudflare/email/management/mailboxes/message.html.erb +34 -0
- data/app/views/cloudflare/email/management/mailboxes/show.html.erb +86 -0
- data/app/views/layouts/cloudflare/email/management.html.erb +29 -0
- data/docs/activerecord-tenanted.md +181 -0
- data/docs/architecture.md +105 -0
- data/docs/custom-ingress.md +207 -0
- data/docs/delivery-events.md +186 -0
- data/docs/features.md +138 -0
- data/docs/getting-started.md +308 -0
- data/docs/mailboxes.md +363 -0
- data/docs/management-engine.md +190 -0
- data/docs/outbox.md +175 -0
- data/docs/routing-deliveries.md +94 -0
- data/docs/routing-diagnostics.md +68 -0
- data/docs/tenant-mailboxes-plan.md +149 -0
- data/docs/thread-correlation.md +27 -0
- data/docs/troubleshooting.md +104 -0
- data/docs/upgrading-0.2.md +101 -0
- data/docs/upgrading-0.3.md +55 -0
- data/docs/verification/2026-09-10-followup.md +49 -0
- data/docs/verification/2026-09-10-install-upgrade.md +86 -0
- data/docs/verification/2026-09-10-live.md +102 -0
- data/docs/verification/2026-09-10.md +175 -0
- data/docs/verification/2026-09-11-custom-ingress.md +49 -0
- data/docs/verification/2026-09-11-extraction.md +32 -0
- data/docs/verification/2026-09-11-outbound-ledger.md +60 -0
- data/docs/verification/2026-09-11-security.md +76 -0
- data/docs/verification/2026-09-11-tenant-mailboxes.md +75 -0
- data/docs/verification/2026-09-12-destination-context.md +46 -0
- data/docs/verification/2026-09-13-durable-inbound-and-routing.md +71 -0
- data/examples/plain_ruby.rb +24 -0
- data/lib/cloudflare/email/active_record/base.rb +60 -0
- data/lib/cloudflare/email/active_record/delivery_events.rb +72 -0
- data/lib/cloudflare/email/active_record/event_inbox.rb +77 -0
- data/lib/cloudflare/email/active_record/event_receipt.rb +17 -0
- data/lib/cloudflare/email/active_record/mail_snapshot.rb +24 -0
- data/lib/cloudflare/email/active_record/outbound_delivery.rb +22 -0
- data/lib/cloudflare/email/active_record/outbound_recipient.rb +13 -0
- data/lib/cloudflare/email/active_record/outbound_reconciliation.rb +15 -0
- data/lib/cloudflare/email/active_record/outbox.rb +219 -0
- data/lib/cloudflare/email/active_record/outbox_notifications.rb +34 -0
- data/lib/cloudflare/email/active_record/routing_deliveries.rb +128 -0
- data/lib/cloudflare/email/active_record.rb +5 -0
- data/lib/cloudflare/email/check_route_task.rb +21 -0
- data/lib/cloudflare/email/client.rb +93 -18
- data/lib/cloudflare/email/consume_events_task.rb +22 -0
- data/lib/cloudflare/email/delivery_event.rb +70 -0
- data/lib/cloudflare/email/delivery_method.rb +5 -0
- data/lib/cloudflare/email/deploy_worker_task.rb +6 -3
- data/lib/cloudflare/email/dev_ingress_guard.rb +27 -0
- data/lib/cloudflare/email/dev_tunnel.rb +31 -5
- data/lib/cloudflare/email/doctor.rb +16 -38
- data/lib/cloudflare/email/endpoint.rb +24 -0
- data/lib/cloudflare/email/engine.rb +33 -0
- data/lib/cloudflare/email/envelope.rb +58 -0
- data/lib/cloudflare/email/event_consumer.rb +95 -0
- data/lib/cloudflare/email/ingress.rb +115 -0
- data/lib/cloudflare/email/mailboxes/configuration.rb +58 -0
- data/lib/cloudflare/email/mailboxes/events.rb +112 -0
- data/lib/cloudflare/email/mailboxes/inbound_retention.rb +14 -0
- data/lib/cloudflare/email/mailboxes/jobs.rb +34 -0
- data/lib/cloudflare/email/mailboxes/models.rb +165 -0
- data/lib/cloudflare/email/mailboxes/provider_correlation.rb +11 -0
- data/lib/cloudflare/email/mailboxes/service.rb +372 -0
- data/lib/cloudflare/email/mailboxes/shared_event_receipt.rb +14 -0
- data/lib/cloudflare/email/mailboxes.rb +11 -0
- data/lib/cloudflare/email/management/adapter.rb +31 -0
- data/lib/cloudflare/email/management/configuration.rb +17 -0
- data/lib/cloudflare/email/management/engine.rb +15 -0
- data/lib/cloudflare/email/management/management.css +68 -0
- data/lib/cloudflare/email/management/routes.rb +14 -0
- data/lib/cloudflare/email/management.rb +7 -0
- data/lib/cloudflare/email/message_id.rb +12 -0
- data/lib/cloudflare/email/provider_metadata.rb +77 -0
- data/lib/cloudflare/email/provision_catchall_task.rb +4 -4
- data/lib/cloudflare/email/provision_route_task.rb +2 -1
- data/lib/cloudflare/email/replay_events_job.rb +18 -0
- data/lib/cloudflare/email/response.rb +21 -3
- data/lib/cloudflare/email/routing_analytics.rb +196 -0
- data/lib/cloudflare/email/routing_diagnostics.rb +206 -0
- data/lib/cloudflare/email/routing_provisioner.rb +71 -45
- data/lib/cloudflare/email/send_job.rb +41 -0
- data/lib/cloudflare/email/send_test.rb +5 -29
- data/lib/cloudflare/email/signing.rb +2 -2
- data/lib/cloudflare/email/tenancy.rb +78 -0
- data/lib/cloudflare/email/tenant_job_context.rb +90 -0
- data/lib/cloudflare/email/verification.rb +38 -8
- data/lib/cloudflare/email/version.rb +1 -1
- data/lib/cloudflare/email/worker_deployer.rb +41 -8
- data/lib/cloudflare-email.rb +4 -1
- data/lib/generators/cloudflare/email/install_generator.rb +53 -37
- data/lib/generators/cloudflare/email/mailboxes/catch_all/catch_all_generator.rb +21 -0
- data/lib/generators/cloudflare/email/mailboxes/catch_all/templates/add_cloudflare_email_catch_all.rb +18 -0
- data/lib/generators/cloudflare/email/mailboxes/mailboxes_generator.rb +55 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_mailboxes.rb +58 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_receiving_domains.rb +16 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_shared_events.rb +35 -0
- data/lib/generators/cloudflare/email/outbox/outbox_generator.rb +28 -0
- data/lib/generators/cloudflare/email/outbox/templates/create_cloudflare_email_outbox.rb +48 -0
- data/lib/generators/cloudflare/email/routing_tracking/routing_tracking_generator.rb +28 -0
- data/lib/generators/cloudflare/email/routing_tracking/templates/create_cloudflare_email_routing_delivery_receipts.rb +22 -0
- data/lib/generators/cloudflare/email/templates/initializer.rb +3 -3
- data/lib/generators/cloudflare/email/templates/main_mailbox.rb +2 -7
- data/lib/generators/cloudflare/email/tracking/templates/create_cloudflare_email_event_receipts.rb +22 -0
- data/lib/generators/cloudflare/email/tracking/tracking_generator.rb +26 -0
- data/lib/tasks/cloudflare_email.rake +42 -4
- data/lib/tasks/cloudflare_email_diagnostics.rake +11 -0
- data/templates/deploy-to-cloudflare/.dev.vars.example +3 -0
- data/templates/deploy-to-cloudflare/.gitignore +6 -0
- data/templates/deploy-to-cloudflare/LICENSE.txt +21 -0
- data/templates/deploy-to-cloudflare/README.md +84 -0
- data/templates/deploy-to-cloudflare/package-lock.json +2627 -0
- data/templates/deploy-to-cloudflare/package.json +36 -0
- data/templates/deploy-to-cloudflare/src/index.js +445 -0
- data/templates/deploy-to-cloudflare/test/durable.test.ts +193 -0
- data/templates/deploy-to-cloudflare/test/index.test.ts +309 -0
- data/templates/deploy-to-cloudflare/test/relay.test.ts +226 -0
- data/templates/deploy-to-cloudflare/vitest.config.ts +8 -0
- data/templates/deploy-to-cloudflare/wrangler.toml +22 -0
- data/templates/worker/README.md +231 -16
- data/templates/worker/docs/durable-inbound.md +149 -0
- data/templates/worker/package-lock.json +2627 -0
- data/templates/worker/package.json +8 -5
- data/templates/worker/scripts/wrangler.mjs +16 -0
- data/templates/worker/src/index.js +397 -25
- data/templates/worker/test/durable.test.ts +193 -0
- data/templates/worker/test/index.test.ts +183 -13
- data/templates/worker/test/relay.test.ts +226 -0
- data/templates/worker/wrangler.toml +63 -4
- metadata +154 -7
- data/lib/cloudflare/email/secure_message_id.rb +0 -89
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Delivery events
|
|
2
|
+
|
|
3
|
+
Cloudflare Email Sending can publish outbound lifecycle events to Queues. This is separate from inbound Email Routing and the ActionMailbox ingress.
|
|
4
|
+
|
|
5
|
+
Mail to a verified Email Routing destination can travel through Routing without
|
|
6
|
+
producing an Email Sending callback. Keep using the recipient's normal address;
|
|
7
|
+
do not interpret a missing callback as failure or resend the message. The optional
|
|
8
|
+
[Routing delivery confirmation](routing-deliveries.md) integration checks
|
|
9
|
+
authenticated analytics for qualifying single-recipient deliveries. It requires
|
|
10
|
+
a separate **Analytics Read + Zone Read** credential and leaves unsupported or
|
|
11
|
+
ambiguous evidence unresolved. Acceptance, receiving-server delivery and a human
|
|
12
|
+
reading the message are different facts.
|
|
13
|
+
|
|
14
|
+
## Provision once
|
|
15
|
+
|
|
16
|
+
1. Create a dedicated Cloudflare Queue for your application's sending events.
|
|
17
|
+
2. In that queue's **Subscriptions** tab, subscribe to Email Sending events for your verified sending domain. Subscriptions are domain-specific; add another for each sending domain you need.
|
|
18
|
+
3. Select delivered, deferred, bounced, failed, rejected, and complained events.
|
|
19
|
+
4. Enable **HTTP pull** as the queue consumer. Do not also attach a Worker push consumer.
|
|
20
|
+
5. Configure queue retries and a dead-letter queue so failed or unsupported events can be investigated instead of disappearing after retry exhaustion.
|
|
21
|
+
6. Create an account-scoped **Queues Read + Write** token (dashboard Queues Edit). Writing is necessary to acknowledge messages.
|
|
22
|
+
|
|
23
|
+
These are explicit Cloudflare setup steps, not actions performed by installing this gem. See [subscription management](https://developers.cloudflare.com/queues/event-subscriptions/manage-event-subscriptions/) and [HTTP pull consumers](https://developers.cloudflare.com/queues/configuration/pull-consumers/).
|
|
24
|
+
|
|
25
|
+
## Rails setup
|
|
26
|
+
|
|
27
|
+
Add credentials:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
cloudflare:
|
|
31
|
+
account_id: your-account-id
|
|
32
|
+
queues_token: your-queues-read-write-token
|
|
33
|
+
event_queue_id: your-queue-id
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Environment alternatives are `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_QUEUES_TOKEN`, and `CLOUDFLARE_EVENT_QUEUE_ID`. Queue consumption uses its own token; it does not use the management-token fallback.
|
|
37
|
+
|
|
38
|
+
Configure a callable application handler:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# config/initializers/cloudflare_delivery_events.rb
|
|
42
|
+
Rails.application.configure do
|
|
43
|
+
config.x.cloudflare_email.event_domains = ["mail.example.com"]
|
|
44
|
+
config.x.cloudflare_email.event_handler = ->(event) {
|
|
45
|
+
DeliveryEventProcessor.call(event)
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`DeliveryEventProcessor` supplies application correlation and policy. For Rails,
|
|
51
|
+
use the optional durable receipt adapter below to avoid implementing receipt
|
|
52
|
+
storage, deduplication and replay yourself. Custom handlers must persist durably
|
|
53
|
+
before returning; do not merely enqueue a non-durable job.
|
|
54
|
+
|
|
55
|
+
### Durable Rails receipts
|
|
56
|
+
|
|
57
|
+
If you also use the [durable outbox](outbox.md), prefer `DeliveryEvents.record`
|
|
58
|
+
and `DeliveryEvents.replay`: they provide account/message/recipient correlation
|
|
59
|
+
and state ordering automatically. The lower-level example below supports an
|
|
60
|
+
application's own delivery schema.
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
bin/rails generate cloudflare:email:tracking
|
|
64
|
+
bin/rails db:migrate
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The generator installs a receipt table and an initializer that explicitly loads
|
|
68
|
+
`cloudflare/email/active_record/event_inbox`. ActiveRecord is not loaded by the
|
|
69
|
+
plain Ruby client. Configure the queue handler to commit receipts:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Rails.application.config.x.cloudflare_email.event_handler = ->(event) {
|
|
73
|
+
Cloudflare::Email::ActiveRecord::EventInbox.record(event)
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Run replay from your recurring job and after saving a send's provider ID:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
Cloudflare::Email::ActiveRecord::EventInbox.replay(account_id: account_id) do |event|
|
|
81
|
+
# Your own model; always scope by account, message AND recipient.
|
|
82
|
+
delivery = RecipientDelivery.find_by(
|
|
83
|
+
account_id: event.account_id,
|
|
84
|
+
provider_message_id: Cloudflare::Email::MessageId.normalize(event.message_id),
|
|
85
|
+
recipient: event.recipient,
|
|
86
|
+
)
|
|
87
|
+
next :unmatched unless delivery
|
|
88
|
+
|
|
89
|
+
delivery.with_lock do
|
|
90
|
+
if event.supersedes?(occurred_at: delivery.occurred_at, terminal: delivery.terminal?)
|
|
91
|
+
delivery.update!(status: event.status, occurred_at: event.occurred_at,
|
|
92
|
+
terminal: event.terminal?)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
:applied
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`record` stores the raw event and enforces uniqueness by account/event ID. An ID
|
|
100
|
+
repeated with different content raises. Recording inside an existing database
|
|
101
|
+
transaction raises, because returning before that transaction commits would allow
|
|
102
|
+
premature acknowledgment. An acknowledged receipt can still be pending or unmatched;
|
|
103
|
+
monitor and schedule replay separately from queue polling.
|
|
104
|
+
|
|
105
|
+
`apply(receipt) { |event| ... }` and `replay` lock each receipt and skip applied
|
|
106
|
+
records. Handlers must return `:applied` or `:unmatched`; exceptions and other return
|
|
107
|
+
values roll back the receipt transition and writes on the same database connection.
|
|
108
|
+
`:unmatched` is a successful transaction outcome, so avoid business writes before
|
|
109
|
+
returning it. External effects and writes to other databases cannot be rolled back.
|
|
110
|
+
Concurrent database conflicts raise; retry the job. Do not send email inside this
|
|
111
|
+
handler; use durable outbound orchestration.
|
|
112
|
+
|
|
113
|
+
Replay accepts optional `account_id:`, normalized provider `message_id:`, and
|
|
114
|
+
`batch_size:` filters. The batch size controls database fetch size, not the total
|
|
115
|
+
number processed. `DeliveryEvent#supersedes?` accepts a Time or ISO8601 previous
|
|
116
|
+
timestamp: only a newer known status replaces current state, equal timestamps keep
|
|
117
|
+
existing state, and a nonterminal event cannot replace terminal state. A later
|
|
118
|
+
terminal complaint can replace delivery. Invalid timestamps raise. Match records
|
|
119
|
+
and lock recipient state before using this ordering helper.
|
|
120
|
+
|
|
121
|
+
Receipt retention, recurring jobs, and monitoring are application decisions. See
|
|
122
|
+
the [gem/inbox boundary](architecture.md) for outbound-ledger scope. Existing
|
|
123
|
+
receipt tables from the earlier preproduction adapter must normalize stored
|
|
124
|
+
`message_id` values with `MessageId.normalize` before using filtered replay;
|
|
125
|
+
raw payloads remain unchanged. The reference inbox's outbox migration does this.
|
|
126
|
+
|
|
127
|
+
Poll a single batch:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
bin/rails cloudflare:email:consume_events
|
|
131
|
+
BATCH_SIZE=20 bin/rails cloudflare:email:consume_events
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Run the task from your existing scheduler, or invoke `EventConsumer#poll` from a recurring job. It short-polls once and exits; an empty queue is a successful result. No handler means an error before any queue request, not automatic discard.
|
|
135
|
+
|
|
136
|
+
For more control, use the API directly:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
consumer = Cloudflare::Email::EventConsumer.new(
|
|
140
|
+
account_id: Cloudflare::Email::Credentials.account_id,
|
|
141
|
+
api_token: Cloudflare::Email::Credentials.fetch(:queues_token),
|
|
142
|
+
queue_id: Cloudflare::Email::Credentials.fetch(:event_queue_id),
|
|
143
|
+
domains: ["mail.example.com"],
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
count = consumer.poll(batch_size: 5, visibility_timeout_ms: 300_000) do |event|
|
|
147
|
+
DeliveryEventProcessor.call(event)
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The same API works in plain Ruby with explicit credentials.
|
|
152
|
+
|
|
153
|
+
## Event data
|
|
154
|
+
|
|
155
|
+
| Method | Meaning |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `event_id` | Durable application deduplication key |
|
|
158
|
+
| `message_id` | Cloudflare message ID for correlation with the send response |
|
|
159
|
+
| `status` | delivered, deferred, bounced, failed, rejected, complained |
|
|
160
|
+
| `known?` | Whether status is one of those six current types |
|
|
161
|
+
| `terminal?` | Cloudflare's terminal flag; not a guarantee no later complaint arrives |
|
|
162
|
+
| `sender`, `recipient` | Addresses from the event |
|
|
163
|
+
| `account_id`, `domain` | Event source, checked against consumer configuration |
|
|
164
|
+
| `occurred_at` | Original event timestamp string |
|
|
165
|
+
| `delivery`, `bounce`, `rejection`, `failure`, `complaint` | Detail hashes |
|
|
166
|
+
| `raw`, `payload`, `type` | Original provider data |
|
|
167
|
+
|
|
168
|
+
Record outcomes per message **and recipient**. A delivery event means the recipient's server accepted the message, not that the person read it. Use bounce/complaint events to maintain your application's recipient eligibility and unsubscribe state. Cloudflare's own suppression behavior remains controlled by Cloudflare; the consumer does not automatically edit provider suppression lists or your database.
|
|
169
|
+
|
|
170
|
+
Events can be duplicated or arrive out of order. Do not blindly overwrite a newer terminal state with an older deferred event. Decide how your application handles a complaint after delivery. Provider IDs may be absent from older send responses; retain event data even when an application record cannot yet be found.
|
|
171
|
+
|
|
172
|
+
## Processing guarantees
|
|
173
|
+
|
|
174
|
+
For each message, the consumer decodes the body, validates the event, invokes your handler, and then acknowledges its lease. Email Sending subscriptions can return plain JSON strings with `CF-Content-Type: json`; the consumer also accepts base64-encoded JSON for that content type. `bytes` bodies contain base64-encoded JSON, while `text` bodies contain JSON directly. Invalid bodies and events remain unacknowledged.
|
|
175
|
+
|
|
176
|
+
- A handler that returns normally counts as successful, even if its return value is `false` or `nil`. **Raise** to prevent acknowledgement.
|
|
177
|
+
- A parse error, mismatched account/domain, handler exception, or acknowledgement failure stops the batch. Previously acknowledged messages remain acknowledged; the current and remaining leases become available after the visibility timeout.
|
|
178
|
+
- If the handler commits and acknowledgement fails, the event can be processed again. This is why the handler needs durable idempotency.
|
|
179
|
+
- Automatic HTTP retries are disabled for queue calls. Your scheduler can retry the next poll.
|
|
180
|
+
- The default batch size is 5 (max 100); the default visibility window is five minutes. Choose a window longer than the **whole batch's** worst-case processing time, up to Cloudflare's 12-hour maximum.
|
|
181
|
+
- Unknown future status names in schema version 1 are passed through with `known? == false`. Decide whether to store or raise. Unsupported schema versions are rejected and left unacknowledged.
|
|
182
|
+
- Configure dead-letter retention/alerts. Repeated failure eventually exhausts the queue's retry policy.
|
|
183
|
+
|
|
184
|
+
The `cloudflare_email.delivery_event` notification wraps handler execution with event ID, message ID, and status. It does not mean acknowledgement succeeded. Monitor task/job failures as well.
|
|
185
|
+
|
|
186
|
+
Source schemas: [Email Sending event subscriptions](https://developers.cloudflare.com/email-service/platform/event-subscriptions/), [pull API](https://developers.cloudflare.com/api/resources/queues/subresources/messages/methods/pull/), [ack API](https://developers.cloudflare.com/api/resources/queues/subresources/messages/methods/ack/).
|
data/docs/features.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
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.3.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
|
+
## Integration tools
|
|
33
|
+
|
|
34
|
+
[Durable inbound delivery](../templates/worker/README.md#durable-inbound-delivery)
|
|
35
|
+
saves raw email and its envelope in R2 before the Worker returns successfully.
|
|
36
|
+
Queues attempt the Rails handoff, and a scheduled pass recovers retained mail
|
|
37
|
+
after enqueue failures or retry exhaustion. Provision the storage, queue and
|
|
38
|
+
schedule before deploying; direct forwarding requires an explicit fallback setting.
|
|
39
|
+
|
|
40
|
+
[Routing delivery confirmation](routing-deliveries.md) adds a read-only provider
|
|
41
|
+
client and optional durable receipts for qualifying single-recipient deliveries
|
|
42
|
+
that appear in Routing analytics. It does not infer outcomes from missing events
|
|
43
|
+
or change whether an accepted email may be sent again.
|
|
44
|
+
|
|
45
|
+
Existing ingestion pipelines can use [custom ingress](custom-ingress.md) to
|
|
46
|
+
verify bounded raw messages before tenant lookup, keep their own storage, or
|
|
47
|
+
persist through ActionMailbox. Optional v3 signatures authenticate custom Worker
|
|
48
|
+
metadata; your application still decides whether its provenance and sender policy
|
|
49
|
+
are acceptable. The bundled Worker continues to use v2 by default.
|
|
50
|
+
|
|
51
|
+
`Mailboxes.receive` yields the resolved destination while retaining email and
|
|
52
|
+
membership persistence, so your own processing record can link to the saved mail.
|
|
53
|
+
`Mailboxes.with_recipient` offers the same destination context for applications
|
|
54
|
+
that own their storage. Neither replaces your sender policy or business workflow.
|
|
55
|
+
|
|
56
|
+
[Read-only routing diagnostics](routing-diagnostics.md) inspect exact-domain DNS
|
|
57
|
+
and the selected Worker route. They distinguish missing configuration from an
|
|
58
|
+
incomplete inspection; a passing snapshot does not prove live email delivery.
|
|
59
|
+
|
|
60
|
+
## SQLite works
|
|
61
|
+
|
|
62
|
+
The optional receipt and outbox tables work with **SQLite**. Keep your existing
|
|
63
|
+
Rails SQLite database; PostgreSQL is optional, and both adapters have concurrency
|
|
64
|
+
coverage. The gem uses ActiveRecord and does not install a separate database
|
|
65
|
+
service. You still choose a durable Rails job backend and run its workers.
|
|
66
|
+
|
|
67
|
+
Database locking conflicts can require a job retry. A retry uses the existing
|
|
68
|
+
outbox identity; it does not clear an uncertain send claim. Keep application
|
|
69
|
+
writes and gem callbacks on the same database connection when they must commit
|
|
70
|
+
together. See [outbox setup](outbox.md).
|
|
71
|
+
|
|
72
|
+
## Understand the different kinds of success
|
|
73
|
+
|
|
74
|
+
| Result | What it tells you |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| Successful API request | Cloudflare returned a successful response; inspect recipient outcomes too |
|
|
77
|
+
| `response.accepted?` | There is evidence of acceptance for at least part of the send |
|
|
78
|
+
| Outbox `accepted` | Every recipient has acceptance evidence |
|
|
79
|
+
| Outbox `partial` | Some recipients have acceptance evidence; the others need individual attention |
|
|
80
|
+
| Delivery event `delivered` | The recipient's server accepted the email; this is not a read receipt |
|
|
81
|
+
| Ingress HTTP 200 | Rails stored the message or recognized a duplicate; mailbox processing happens separately |
|
|
82
|
+
|
|
83
|
+
Sending, receiving and delivery tracking are separate Cloudflare setups. A
|
|
84
|
+
working sending domain does not automatically enable incoming routes or queue
|
|
85
|
+
subscriptions. The [getting-started guide](getting-started.md) walks through each.
|
|
86
|
+
|
|
87
|
+
## Protections included
|
|
88
|
+
|
|
89
|
+
By default, incoming requests use v2 HMAC signatures covering the timestamp, SMTP sender,
|
|
90
|
+
SMTP recipient and raw message. Rails checks a five-minute timestamp window.
|
|
91
|
+
Identical MIME retried for the same recipient is deduplicated; delivery to a
|
|
92
|
+
different recipient is stored separately.
|
|
93
|
+
|
|
94
|
+
Rails and the Worker default to a **25 MiB raw incoming message limit**. Both
|
|
95
|
+
support a positive `MAX_EMAIL_BYTES` override. Remote endpoints require HTTPS,
|
|
96
|
+
the Worker refuses redirects and limits its Rails request to 15 seconds, and
|
|
97
|
+
the development tunnel only routes ingress POSTs. Client inspection and default
|
|
98
|
+
retry/mailbox logging omit sensitive details.
|
|
99
|
+
|
|
100
|
+
The outbox preserves the rendered message and acceptance evidence, blocks
|
|
101
|
+
ambiguous resend attempts, and records reconciliation decisions. Delivery-event
|
|
102
|
+
validation rejects malformed schema fields; original receipt identity and
|
|
103
|
+
payload are read-only through normal ActiveRecord updates. These safeguards do
|
|
104
|
+
not make database administrators untrusted or provide exactly-once delivery.
|
|
105
|
+
|
|
106
|
+
See [managed mailbox setup](mailboxes.md) for the application-facing API and
|
|
107
|
+
[activerecord-tenanted](activerecord-tenanted.md) for separate SQLite databases.
|
|
108
|
+
|
|
109
|
+
## What belongs in your mailbox application
|
|
110
|
+
|
|
111
|
+
The gem supplies email infrastructure and optional managed mailbox records.
|
|
112
|
+
Your app supplies users, mailbox permissions, conversation records, custom folders, search, compose screens,
|
|
113
|
+
drafts and any AI review or approval workflow. It also decides unsubscribe and
|
|
114
|
+
recipient eligibility policy. The optional [management engine](management-engine.md)
|
|
115
|
+
adds server-rendered mailbox administration and plain-text message previews.
|
|
116
|
+
It does not supply a compose/conversation product or AI agent.
|
|
117
|
+
|
|
118
|
+
An authenticated SMTP envelope tells your app which address Cloudflare received
|
|
119
|
+
the message for. It does not prove the human sender's identity. Reply correlation
|
|
120
|
+
also does not authorize access to a conversation. Apply your app's permissions
|
|
121
|
+
before displaying mail or sending a reply.
|
|
122
|
+
|
|
123
|
+
You configure Cloudflare domains/DNS, queue subscriptions, durable jobs,
|
|
124
|
+
monitoring, storage protection and retention. Direct forwarding does not buffer
|
|
125
|
+
Rails outages; the optional durable inbound setup retains pending mail in R2.
|
|
126
|
+
See [architecture](architecture.md)
|
|
127
|
+
for the full boundary and [security guidance](../SECURITY.md) for deployment.
|
|
128
|
+
|
|
129
|
+
## Where to go next
|
|
130
|
+
|
|
131
|
+
- [Build your first integration](getting-started.md)
|
|
132
|
+
- [Use plain Ruby, attachments or raw MIME](../README.md#plain-ruby)
|
|
133
|
+
- [Configure client retries and errors](../README.md#retry-and-configuration)
|
|
134
|
+
- [Set up event polling and durable receipts](delivery-events.md)
|
|
135
|
+
- [Recover saved outbound operations](outbox.md)
|
|
136
|
+
- [Correlate replies](thread-correlation.md)
|
|
137
|
+
- [Use Cloudflare SMTP instead of the HTTP delivery method](../README.md#smtp-alternative)
|
|
138
|
+
- [Troubleshoot your setup](troubleshooting.md)
|
|
@@ -0,0 +1,308 @@
|
|
|
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.3 to your app's `Gemfile`. If upgrading an existing installation, read the
|
|
22
|
+
[upgrade guide](upgrading-0.3.md) before changing an existing Worker deployment:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
gem "cloudflare-email", "~> 0.3.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. For guided
|
|
127
|
+
Worker setup, use the [Deploy to Cloudflare template](../templates/deploy-to-cloudflare/README.md).
|
|
128
|
+
It provisions R2 and Queues and prompts for your Rails URL and shared secret.
|
|
129
|
+
Then select that Worker in your Email Routing rule as described in its guide.
|
|
130
|
+
|
|
131
|
+
For the CLI setup instead, run these commands in an environment with production
|
|
132
|
+
configuration and deployment credentials:
|
|
133
|
+
|
|
134
|
+
First create the private R2 bucket and Queue, then deploy the generated Wrangler
|
|
135
|
+
configuration using the [durable setup guide](../templates/worker/docs/durable-inbound.md).
|
|
136
|
+
The Ruby task below updates that provisioned Worker while preserving its bindings.
|
|
137
|
+
It refuses a missing bucket/queue binding or recovery schedule before changing code.
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
RAILS_ENV=production bin/rails cloudflare:email:deploy_worker URL=https://app.example.com/rails/action_mailbox/cloudflare/inbound_emails
|
|
141
|
+
RAILS_ENV=production bin/rails cloudflare:email:provision_route ADDRESS=support@in.example.com
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The first command deploys the Worker; the second routes the address to it.
|
|
145
|
+
Send a real test email to `support@in.example.com`, then check the ActionMailbox
|
|
146
|
+
record and processing job. A web request accepted by Rails means storage worked;
|
|
147
|
+
the mailbox job can still need attention.
|
|
148
|
+
|
|
149
|
+
### Read the message in your mailbox
|
|
150
|
+
|
|
151
|
+
Inside the generated `MainMailbox#process`, these values are available:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
envelope = Cloudflare::Email::Envelope.for(inbound_email)
|
|
155
|
+
receiving_address = envelope&.fetch("to")
|
|
156
|
+
subject = mail.subject
|
|
157
|
+
text = mail.text_part&.body&.decoded || (mail.body.decoded unless mail.multipart?)
|
|
158
|
+
attachments = mail.attachments
|
|
159
|
+
raw_message = inbound_email.raw_email.download
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Save or process those values using your application's models. For mailbox or
|
|
163
|
+
tenant selection, use `receiving_address`, which comes from the authenticated
|
|
164
|
+
SMTP envelope. MIME `To`/`Cc` headers may differ, especially for Bcc. The envelope
|
|
165
|
+
can be absent for records from another ingress; handle that case explicitly.
|
|
166
|
+
Treat message contents and attachments as untrusted input when displaying them.
|
|
167
|
+
|
|
168
|
+
To direct replies to this address, add `reply_to: "support@in.example.com"` to
|
|
169
|
+
your mailer's `mail(...)` call. For conversation matching, save the returned
|
|
170
|
+
provider message ID and compare reply headers within your application's mailbox
|
|
171
|
+
scope. The outbox saves this ID for you. See [thread correlation](thread-correlation.md).
|
|
172
|
+
|
|
173
|
+
### Receive into a local Rails server
|
|
174
|
+
|
|
175
|
+
Install `cloudflared`, configure development credentials and a separate development
|
|
176
|
+
receiving route, then start Rails. In another terminal:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
INBOUND_DELIVERY_MODE=direct bin/rails cloudflare:email:deploy_worker
|
|
180
|
+
bin/rails cloudflare:email:dev
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The first command creates the development Worker with the explicit direct fallback; the second points it at a
|
|
184
|
+
temporary tunnel to your local Rails server on port 3000. Keep the tunnel running
|
|
185
|
+
while testing and stop it with Ctrl-C. Set `PORT=...` if Rails uses another port.
|
|
186
|
+
The task checks that Rails has the ingress-only guard; restart Rails if asked.
|
|
187
|
+
Use development test data. The tunnel URL is temporary and does not provide an
|
|
188
|
+
offline mailbox when your local server is stopped.
|
|
189
|
+
|
|
190
|
+
## 3. Add the durable outbox with SQLite
|
|
191
|
+
|
|
192
|
+
An outbox saves the exact message before sending it. Background jobs reference
|
|
193
|
+
that saved operation instead of re-rendering and resending mail on every retry.
|
|
194
|
+
This is useful for compose, replies, invoices and approved drafts.
|
|
195
|
+
|
|
196
|
+
If your Rails app already uses SQLite, keep its database configuration. Run:
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
bin/rails generate cloudflare:email:tracking
|
|
200
|
+
bin/rails generate cloudflare:email:outbox
|
|
201
|
+
bin/rails db:migrate
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
These add the receipt and delivery tables plus opt-in initializers. Skip a
|
|
205
|
+
generator whose migration you already installed. Configure a durable ActiveJob
|
|
206
|
+
backend and run a worker that processes the `mailers` queue; the gem does not
|
|
207
|
+
install the backend for you.
|
|
208
|
+
|
|
209
|
+
Try this from Rails console, outside an existing database transaction:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
account_id = Cloudflare::Email::Credentials.account_id
|
|
213
|
+
operation = Cloudflare::Email::ActiveRecord::Outbox.prepare_mail(
|
|
214
|
+
account_id: account_id,
|
|
215
|
+
operation_key: "getting-started:hello:1",
|
|
216
|
+
mail: HelloMailer.hello("you@example.net").message,
|
|
217
|
+
)
|
|
218
|
+
Cloudflare::Email::SendJob.perform_later(account_id, operation.operation_key)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The operation key identifies **one intended send** in your account. Save it with
|
|
222
|
+
your product record. For a genuinely new message use a new key; for a job retry
|
|
223
|
+
reuse the saved identity. Do not re-render a mailer and call `prepare_mail` as
|
|
224
|
+
your retry mechanism: generated MIME headers can change, causing a snapshot conflict.
|
|
225
|
+
|
|
226
|
+
In an application transaction, prepare the operation alongside your business
|
|
227
|
+
record, then enqueue only after the outer transaction commits. The detailed
|
|
228
|
+
[outbox guide](outbox.md#prepare-once-dispatch-after-commit) shows that pattern.
|
|
229
|
+
|
|
230
|
+
After the worker runs, inspect the result:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
operation.reload.state
|
|
234
|
+
operation.provider_message_id
|
|
235
|
+
operation.outbound_recipients.pluck(:recipient, :acceptance_state, :state)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`accepted` means all recipients have provider acceptance evidence; `partial`
|
|
239
|
+
means only some do. Neither promises that a human received or read the message.
|
|
240
|
+
`unknown` or a stuck `sending` operation needs investigation, not a new send key
|
|
241
|
+
as a shortcut. See [states and recovery](outbox.md#states-and-retries).
|
|
242
|
+
|
|
243
|
+
## 4. Track delivery, bounces and complaints
|
|
244
|
+
|
|
245
|
+
Cloudflare sends later delivery updates through a Queue. You configure that
|
|
246
|
+
Queue and its Email Sending subscription once; your app polls it regularly.
|
|
247
|
+
Follow [the provisioning steps](delivery-events.md#provision-once), including an
|
|
248
|
+
HTTP pull consumer, retries and a dead-letter queue for repeatedly failing events.
|
|
249
|
+
|
|
250
|
+
Add a separate queue token and queue ID to your Rails environment:
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
export CLOUDFLARE_QUEUES_TOKEN=your-queues-read-write-token
|
|
254
|
+
export CLOUDFLARE_EVENT_QUEUE_ID=your-queue-id
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
With the outbox installed, configure this initializer:
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
# config/initializers/cloudflare_delivery_events.rb
|
|
261
|
+
require "cloudflare/email/active_record"
|
|
262
|
+
|
|
263
|
+
Rails.application.configure do
|
|
264
|
+
config.x.cloudflare_email.event_domains = ["mail.example.com"]
|
|
265
|
+
config.x.cloudflare_email.event_handler = ->(event) {
|
|
266
|
+
Cloudflare::Email::ActiveRecord::DeliveryEvents.record(event)
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Restart Rails and poll a batch:
|
|
272
|
+
|
|
273
|
+
```sh
|
|
274
|
+
bin/rails cloudflare:email:consume_events
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
This command processes **one batch and exits**. Schedule it repeatedly using
|
|
278
|
+
your application's scheduler. Also schedule receipt replay for recovery:
|
|
279
|
+
|
|
280
|
+
```sh
|
|
281
|
+
bin/rails cloudflare:email:replay_events
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The gem saves receipts before acknowledgement, handles duplicates, and matches
|
|
285
|
+
updates to the account, provider message ID and recipient. If an event arrives
|
|
286
|
+
before the send result is saved, it stays available for replay. Recipient state
|
|
287
|
+
then reflects delivered, deferred, bounced, failed, rejected or complained events.
|
|
288
|
+
Your app still decides what those outcomes mean for its UI and future sending.
|
|
289
|
+
|
|
290
|
+
For custom product updates, use the [transactional callbacks](outbox.md#recover-dispatch-and-application-projections).
|
|
291
|
+
If you only want events and have your own delivery models, use the lower-level
|
|
292
|
+
[durable receipt adapter](delivery-events.md#durable-rails-receipts).
|
|
293
|
+
|
|
294
|
+
## Before relying on it
|
|
295
|
+
|
|
296
|
+
Send to an external inbox you control, reply back, verify stored attachments,
|
|
297
|
+
and confirm a real queue event updates the intended recipient. Keep a durable
|
|
298
|
+
job worker running and schedule recovery of `prepared` operations that might
|
|
299
|
+
have committed before enqueueing. Monitor stuck operations and failed mailbox jobs.
|
|
300
|
+
|
|
301
|
+
Protect stored MIME and attachments, choose retention and backup policies, and
|
|
302
|
+
set proxy/server request limits. The default incoming raw message limit is
|
|
303
|
+
25 MiB; set the same positive `MAX_EMAIL_BYTES` on Rails and the Worker if you
|
|
304
|
+
override it. Cloudflare has separate [outgoing size and recipient limits](https://developers.cloudflare.com/email-service/platform/limits/).
|
|
305
|
+
|
|
306
|
+
Use [troubleshooting](troubleshooting.md) when something does not arrive,
|
|
307
|
+
[the feature overview](features.md) to explore more, and
|
|
308
|
+
[the upgrade guide](upgrading-0.3.md) for an existing 0.1 installation.
|