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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -1
- data/README.md +265 -388
- data/SECURITY.md +51 -0
- data/app/controllers/cloudflare/email/ingress_controller.rb +62 -6
- data/docs/activerecord-tenanted.md +135 -0
- data/docs/architecture.md +95 -0
- data/docs/delivery-events.md +177 -0
- data/docs/features.md +107 -0
- data/docs/getting-started.md +298 -0
- data/docs/mailboxes.md +299 -0
- data/docs/outbox.md +175 -0
- data/docs/tenant-mailboxes-plan.md +149 -0
- data/docs/thread-correlation.md +27 -0
- data/docs/troubleshooting.md +103 -0
- data/docs/upgrading-0.2.md +96 -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-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/examples/plain_ruby.rb +24 -0
- data/lib/cloudflare/email/active_record/base.rb +48 -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.rb +5 -0
- data/lib/cloudflare/email/client.rb +51 -11
- 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 +3 -0
- data/lib/cloudflare/email/deploy_worker_task.rb +3 -0
- 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 +28 -0
- data/lib/cloudflare/email/envelope.rb +58 -0
- data/lib/cloudflare/email/event_consumer.rb +86 -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 +148 -0
- data/lib/cloudflare/email/mailboxes/provider_correlation.rb +11 -0
- data/lib/cloudflare/email/mailboxes/service.rb +288 -0
- data/lib/cloudflare/email/mailboxes/shared_event_receipt.rb +14 -0
- data/lib/cloudflare/email/mailboxes.rb +11 -0
- data/lib/cloudflare/email/message_id.rb +12 -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_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 +30 -8
- data/lib/cloudflare/email/version.rb +1 -1
- data/lib/cloudflare/email/worker_deployer.rb +5 -3
- data/lib/cloudflare-email.rb +4 -1
- data/lib/generators/cloudflare/email/install_generator.rb +48 -33
- data/lib/generators/cloudflare/email/mailboxes/mailboxes_generator.rb +55 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_mailboxes.rb +54 -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/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 +41 -4
- data/templates/worker/README.md +61 -11
- 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 +83 -4
- data/templates/worker/test/index.test.ts +117 -5
- data/templates/worker/wrangler.toml +13 -4
- metadata +107 -6
- 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.
|