cloudflare-email 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -1
- data/README.md +63 -369
- data/SECURITY.md +1 -9
- data/app/controllers/cloudflare/email/ingress_controller.rb +1 -2
- data/app/controllers/cloudflare/email/management/mailboxes_controller.rb +2 -166
- data/app/controllers/cloudflare/email/management/styles_controller.rb +2 -6
- data/docs/activerecord-tenanted.md +0 -11
- data/docs/custom-ingress.md +6 -16
- data/docs/delivery-events.md +2 -5
- data/docs/features.md +49 -123
- data/docs/getting-started.md +91 -172
- data/docs/mailboxes.md +27 -56
- data/docs/management-engine.md +1 -3
- data/docs/outbox.md +5 -6
- data/docs/reference.md +190 -0
- data/docs/routing-diagnostics.md +7 -2
- data/docs/thread-correlation.md +2 -4
- data/docs/troubleshooting.md +4 -4
- data/lib/cloudflare/email/active_record/base.rb +3 -53
- data/lib/cloudflare/email/client.rb +23 -5
- data/lib/cloudflare/email/engine.rb +1 -13
- data/lib/cloudflare/email/envelope.rb +2 -12
- data/lib/cloudflare/email/error.rb +5 -12
- data/lib/cloudflare/email/ingress.rb +19 -8
- data/lib/cloudflare/email/mailboxes/configuration.rb +7 -15
- data/lib/cloudflare/email/mailboxes/inbound_retention.rb +1 -14
- data/lib/cloudflare/email/mailboxes/models.rb +8 -147
- data/lib/cloudflare/email/mailboxes/service.rb +7 -258
- data/lib/cloudflare/email/mailboxes.rb +1 -0
- data/lib/cloudflare/email/management/adapter.rb +3 -25
- data/lib/cloudflare/email/management/configuration.rb +4 -11
- data/lib/cloudflare/email/management/engine.rb +2 -0
- data/lib/cloudflare/email/tenancy.rb +2 -73
- data/lib/cloudflare/email/tenant_job_context.rb +2 -85
- data/lib/cloudflare/email/version.rb +1 -1
- data/lib/cloudflare-email.rb +6 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_mailboxes.rb +1 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_receiving_domains.rb +1 -1
- data/templates/deploy-to-cloudflare/README.md +25 -7
- data/templates/deploy-to-cloudflare/docs/domain-setup.md +118 -0
- data/templates/deploy-to-cloudflare/package.json +1 -0
- data/templates/deploy-to-cloudflare/scripts/check-subdomains.mjs +54 -0
- data/templates/deploy-to-cloudflare/test/subdomains.test.ts +51 -0
- data/templates/worker/README.md +11 -7
- data/templates/worker/docs/domain-setup.md +118 -0
- data/templates/worker/docs/durable-inbound.md +4 -16
- data/templates/worker/package.json +1 -0
- data/templates/worker/scripts/check-subdomains.mjs +54 -0
- data/templates/worker/test/subdomains.test.ts +51 -0
- metadata +22 -21
- data/app/views/cloudflare/email/management/mailboxes/index.html.erb +0 -65
- data/app/views/cloudflare/email/management/mailboxes/message.html.erb +0 -34
- data/app/views/cloudflare/email/management/mailboxes/show.html.erb +0 -86
- data/app/views/layouts/cloudflare/email/management.html.erb +0 -29
- data/docs/architecture.md +0 -105
- data/docs/tenant-mailboxes-plan.md +0 -149
- data/docs/upgrading-0.2.md +0 -101
- data/docs/upgrading-0.3.md +0 -55
- data/docs/verification/2026-09-10-followup.md +0 -49
- data/docs/verification/2026-09-10-install-upgrade.md +0 -86
- data/docs/verification/2026-09-10-live.md +0 -102
- data/docs/verification/2026-09-10.md +0 -175
- data/docs/verification/2026-09-11-custom-ingress.md +0 -49
- data/docs/verification/2026-09-11-extraction.md +0 -32
- data/docs/verification/2026-09-11-outbound-ledger.md +0 -60
- data/docs/verification/2026-09-11-security.md +0 -76
- data/docs/verification/2026-09-11-tenant-mailboxes.md +0 -75
- data/docs/verification/2026-09-12-destination-context.md +0 -46
- data/docs/verification/2026-09-13-durable-inbound-and-routing.md +0 -71
- data/lib/cloudflare/email/management/management.css +0 -68
|
@@ -1,172 +1,8 @@
|
|
|
1
|
+
require File.join(MailboxKit::ROOT, "app/controllers/mailbox_kit/management/mailboxes_controller.rb")
|
|
1
2
|
module Cloudflare
|
|
2
3
|
module Email
|
|
3
4
|
module Management
|
|
4
|
-
class MailboxesController <
|
|
5
|
-
layout "layouts/cloudflare/email/management"
|
|
6
|
-
protect_from_forgery with: :exception
|
|
7
|
-
self.forgery_protection_origin_check = true
|
|
8
|
-
around_action :with_host_context
|
|
9
|
-
helper_method :allowed?, :host_back_path
|
|
10
|
-
|
|
11
|
-
class Denied < StandardError; end
|
|
12
|
-
class InvalidInput < StandardError; end
|
|
13
|
-
|
|
14
|
-
rescue_from Denied do
|
|
15
|
-
head :forbidden
|
|
16
|
-
end
|
|
17
|
-
rescue_from "ActiveRecord::RecordNotFound" do
|
|
18
|
-
head :not_found
|
|
19
|
-
end
|
|
20
|
-
rescue_from InvalidInput, "ActiveRecord::RecordInvalid", Cloudflare::Email::Error do
|
|
21
|
-
redirect_to(@mailbox ? mailbox_path(@mailbox) : root_path,
|
|
22
|
-
alert: "The changes could not be saved. Check the address, registered domain and required fields.",
|
|
23
|
-
status: :see_other)
|
|
24
|
-
end
|
|
25
|
-
rescue_from Cloudflare::Email::ConfigurationError do
|
|
26
|
-
head :service_unavailable
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def index
|
|
30
|
-
authorize!(:index)
|
|
31
|
-
@mailboxes = page(scoped_mailboxes)
|
|
32
|
-
@domains = permitted_domains if allowed?(:create)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def show
|
|
36
|
-
load_mailbox!(:show)
|
|
37
|
-
@addresses = @session.addresses(@mailbox.id).order(:id).to_a
|
|
38
|
-
@domains = allowed?(:add_address, @mailbox) ? permitted_domains : []
|
|
39
|
-
@messages = allowed?(:show_message, @mailbox) ? page(@session.messages(@mailbox.id)) : []
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def create
|
|
43
|
-
authorize!(:create)
|
|
44
|
-
input = params.require(:mailbox)
|
|
45
|
-
raise InvalidInput unless input.is_a?(ActionController::Parameters)
|
|
46
|
-
values = input.permit(:name, :address)
|
|
47
|
-
name = values[:name].to_s.strip
|
|
48
|
-
raise InvalidInput unless (1..255).cover?(name.length)
|
|
49
|
-
address = permitted_address!(values[:address])
|
|
50
|
-
Mailboxes::Mailbox.transaction do
|
|
51
|
-
@mailbox = @adapter.create_mailbox(@session, name: name, address: address)
|
|
52
|
-
# A hook cannot turn an arbitrary return value into a readable mailbox.
|
|
53
|
-
@mailbox = scoped_mailboxes.find(@mailbox.id)
|
|
54
|
-
end
|
|
55
|
-
redirect_to mailbox_path(@mailbox), notice: "Mailbox created. Review address setup below.", status: :see_other
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def add_address
|
|
59
|
-
load_mailbox!(:add_address)
|
|
60
|
-
@adapter.add_address(@session, @mailbox, address: permitted_address!(params[:address]))
|
|
61
|
-
redirect_to mailbox_path(@mailbox), notice: "Address added. Review its setup status.", status: :see_other
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def suspend
|
|
65
|
-
load_mailbox!(:suspend)
|
|
66
|
-
@session.suspend(@mailbox.id)
|
|
67
|
-
redirect_to mailbox_path(@mailbox), notice: "Mailbox suspended. Messages are retained.", status: :see_other
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def resume
|
|
71
|
-
load_mailbox!(:resume)
|
|
72
|
-
@session.resume(@mailbox.id)
|
|
73
|
-
redirect_to mailbox_path(@mailbox), notice: "Mailbox resumed. Pending addresses still need setup.", status: :see_other
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def message
|
|
77
|
-
load_mailbox!(:show_message)
|
|
78
|
-
@entry = @session.messages(@mailbox.id).find(params[:message_id])
|
|
79
|
-
raw = @session.inbound_email(@mailbox.id, @entry.id).mail
|
|
80
|
-
@subject = raw.subject.to_s
|
|
81
|
-
@from = Array(raw.from).join(", ")
|
|
82
|
-
part = raw.text_part || (raw.mime_type == "text/plain" ? raw : nil)
|
|
83
|
-
@body = part ? part.decoded.to_s.encode("UTF-8", invalid: :replace, undef: :replace).first(100_000) :
|
|
84
|
-
"This message has no plain-text body. HTML rendering is disabled in this management interface."
|
|
85
|
-
@attachments = raw.attachments.map { |attachment| attachment.filename.to_s }
|
|
86
|
-
render :message
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def mark_read
|
|
90
|
-
load_mailbox!(:mark_read)
|
|
91
|
-
@session.mark_read(@mailbox.id, params[:message_id], read: boolean_param(:read))
|
|
92
|
-
redirect_to mailbox_path(@mailbox), status: :see_other
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def archive
|
|
96
|
-
load_mailbox!(:archive)
|
|
97
|
-
@session.archive(@mailbox.id, params[:message_id], archived: boolean_param(:archived))
|
|
98
|
-
redirect_to mailbox_path(@mailbox), status: :see_other
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
private
|
|
102
|
-
|
|
103
|
-
def with_host_context
|
|
104
|
-
response.headers["Cache-Control"] = "no-store"
|
|
105
|
-
response.headers["Referrer-Policy"] = "same-origin"
|
|
106
|
-
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
107
|
-
response.headers["Content-Security-Policy"] = "default-src 'none'; style-src 'self'; img-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'"
|
|
108
|
-
factory = Management.configuration.adapter
|
|
109
|
-
unless factory.respond_to?(:call) && defined?(Mailboxes) && Mailboxes.enabled?
|
|
110
|
-
return head :service_unavailable
|
|
111
|
-
end
|
|
112
|
-
@adapter = factory.call(self)
|
|
113
|
-
authenticated = @adapter.authenticate!
|
|
114
|
-
return if performed?
|
|
115
|
-
return head :unauthorized unless authenticated == true
|
|
116
|
-
Mailboxes.for_tenant(@adapter.tenant_key) do |session|
|
|
117
|
-
@session = session
|
|
118
|
-
yield
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def allowed?(action, mailbox = nil)
|
|
123
|
-
@adapter.allowed?(action, mailbox) == true
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def authorize!(action, mailbox = nil)
|
|
127
|
-
raise Denied unless allowed?(action, mailbox)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def scoped_mailboxes
|
|
131
|
-
# Always retain the gem's tenant condition even if a host scope omits it.
|
|
132
|
-
@session.mailboxes.where(id: @adapter.mailboxes(@session).reselect(:id))
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def load_mailbox!(action)
|
|
136
|
-
@mailbox = scoped_mailboxes.find(params[:id])
|
|
137
|
-
authorize!(action, @mailbox)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def permitted_domains
|
|
141
|
-
granted = Array(@adapter.domains(@session)).map(&:to_s)
|
|
142
|
-
Mailboxes::ReceivingDomain.active.where(tenant_key: @session.tenant_key, domain: granted).order(:domain).pluck(:domain)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def permitted_address!(value)
|
|
146
|
-
address = Mailboxes.canonical_address(value)
|
|
147
|
-
raise InvalidInput unless permitted_domains.include?(address.split("@", 2).last)
|
|
148
|
-
address
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def page(relation)
|
|
152
|
-
cursor = params[:after].to_s
|
|
153
|
-
raise InvalidInput unless cursor.empty? || cursor.match?(/\A[0-9]{1,18}\z/)
|
|
154
|
-
rows = relation.where("id > ?", cursor.to_i).order(:id).limit(51).to_a
|
|
155
|
-
@next_cursor = rows.length > 50 ? rows[49].id : nil
|
|
156
|
-
rows.first(50)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def boolean_param(key)
|
|
160
|
-
value = params[key]
|
|
161
|
-
raise InvalidInput unless %w[true false].include?(value)
|
|
162
|
-
value == "true"
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def host_back_path
|
|
166
|
-
callback = Management.configuration.back_path
|
|
167
|
-
value = callback.call(self) if callback.respond_to?(:call)
|
|
168
|
-
value if value.is_a?(String) && value.start_with?("/") && !value.start_with?("//") && !value.match?(/[\\\r\n]/)
|
|
169
|
-
end
|
|
5
|
+
class MailboxesController < MailboxKit::Management::MailboxesController
|
|
170
6
|
end
|
|
171
7
|
end
|
|
172
8
|
end
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
+
require File.join(MailboxKit::ROOT, "app/controllers/mailbox_kit/management/styles_controller.rb")
|
|
1
2
|
module Cloudflare
|
|
2
3
|
module Email
|
|
3
4
|
module Management
|
|
4
|
-
class StylesController <
|
|
5
|
-
def show
|
|
6
|
-
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
7
|
-
send_data File.binread(File.expand_path("../../../../../lib/cloudflare/email/management/management.css", __dir__)),
|
|
8
|
-
type: "text/css; charset=utf-8", disposition: "inline"
|
|
9
|
-
end
|
|
5
|
+
class StylesController < MailboxKit::Management::StylesController
|
|
10
6
|
end
|
|
11
7
|
end
|
|
12
8
|
end
|
|
@@ -168,14 +168,3 @@ database. Missing context must continue to fail closed. Your organization's job
|
|
|
168
168
|
fan-out should enumerate real organizations, not every database file. Applications
|
|
169
169
|
without configured multi-tenancy can keep ordinary system and organization mail
|
|
170
170
|
in their existing single database; this recipe does not enable tenancy by default.
|
|
171
|
-
|
|
172
|
-
## What is verified
|
|
173
|
-
|
|
174
|
-
The optional CI job boots a real Rails application with `activerecord-tenanted`, runs the gem's tenant migrations in two SQLite databases, and checks identical numeric IDs remain isolated. It also verifies context restoration, refusal to create unknown tenant databases, and GlobalID's missing/wrong-tenant rejection. Separate integration tests exercise mailbox ingress, sending, jobs, and delivery-event replay.
|
|
175
|
-
|
|
176
|
-
Run the library-specific smoke test locally with:
|
|
177
|
-
|
|
178
|
-
```sh
|
|
179
|
-
BUNDLE_GEMFILE=gemfiles/tenanted.gemfile bundle install
|
|
180
|
-
BUNDLE_GEMFILE=gemfiles/tenanted.gemfile bundle exec ruby test/support/activerecord_tenanted.rb
|
|
181
|
-
```
|
data/docs/custom-ingress.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# Use your existing inbound email pipeline
|
|
2
2
|
|
|
3
|
-
This integration API is available in gem version 0.3.0.
|
|
4
|
-
|
|
5
3
|
You can adopt mailbox registration without replacing your Worker, document processing, sender review, or archive. Use the gem to authenticate the request and resolve an accepted address, then keep your application's policy and processing.
|
|
6
4
|
|
|
7
5
|
A useful starting point is one mailbox per site: an organization owns a receiving domain, a site owns a mailbox, and that mailbox can have several accepted addresses. This is an application convention, not a requirement of the gem. Database tenancy is optional; a single SQLite database works too.
|
|
@@ -42,6 +40,10 @@ class InboundEmailsController < ActionController::API
|
|
|
42
40
|
|
|
43
41
|
verified = result.message
|
|
44
42
|
|
|
43
|
+
# If no custom acceptance/processing record is needed, this is sufficient:
|
|
44
|
+
# verified.receive_into_mailbox!
|
|
45
|
+
# It also repairs missing membership on duplicate delivery without rerouting.
|
|
46
|
+
|
|
45
47
|
Cloudflare::Email::Mailboxes.receive(
|
|
46
48
|
recipient: verified.envelope.fetch("to")
|
|
47
49
|
) do |destination|
|
|
@@ -64,7 +66,7 @@ Mount this controller at your own route and point your existing Worker there. Th
|
|
|
64
66
|
|
|
65
67
|
Do not use the MIME `To` header or an unauthenticated URL subdomain to select storage. `verified.envelope.fetch("to")` is the signed SMTP recipient. `Mailboxes.receive` checks that its domain, mailbox, and accepted address are active before entering the persistence block.
|
|
66
68
|
|
|
67
|
-
`persist_action_mailbox!`
|
|
69
|
+
`persist_action_mailbox!` delegates verified raw bytes and metadata to the core's Rails persistence bridge. It preserves the existing new-record-or-`nil` return convention; the block example therefore does no new work for a duplicate. The default `receive_into_mailbox!` bridge also attaches the existing record, repairing a missing membership without reprocessing. Rails schedules normal routing only for newly created records after the transaction commits. Apply policies that must stop processing before calling persistence; a check performed after receiving returns can be too late.
|
|
68
70
|
|
|
69
71
|
For an application that owns a different raw-email store, use `verified.body`, `verified.envelope`, `verified.provider_metadata`, `verified.message_checksum`, and `verified.storage_metadata` with your own persistence/transaction system. `Mailboxes.receive` specifically expects an Action Mailbox inbound email record (or nil) from its block; do not pass an unrelated processing record. The gem does not choose your archive retention, held-message model, or document queue.
|
|
70
72
|
|
|
@@ -177,7 +179,7 @@ const headers = await signedEmailHeaders({
|
|
|
177
179
|
// Send exactly raw with these headers using your existing transport.
|
|
178
180
|
```
|
|
179
181
|
|
|
180
|
-
Here `cloudflare-ingress.js` is your local copy of the template's `src/index.js`. `raw` and `archiveKey` come from your existing bounded reader and archive operation. See the [Worker integration instructions](../templates/worker/README.md) for supported metadata values and forwarding behavior.
|
|
182
|
+
Here `cloudflare-ingress.js` is your local copy of the template's `src/index.js`. `raw` and `archiveKey` come from your existing bounded reader and archive operation. See the [Worker integration instructions](../templates/worker/README.md) for supported metadata values and forwarding behavior.
|
|
181
183
|
|
|
182
184
|
In your Action Mailbox handler, read persisted metadata through the gem:
|
|
183
185
|
|
|
@@ -193,15 +195,3 @@ This reads verified Active Storage metadata, not similarly named MIME headers. A
|
|
|
193
195
|
**A valid Worker signature proves that your trusted Worker submitted the request. It does not prove the original email sender is authentic.** Even metadata labelled `source: "cloudflare"` is an assertion made by the holder of the ingress secret. Do not copy sender-controlled MIME headers into a “trusted authentication” field. If your existing Worker has reliable authentication evidence, preserve its documented provenance and let your application decide how that evidence affects acceptance. The gem intentionally does not infer SPF/DMARC results or implement a sender allowlist.
|
|
194
196
|
|
|
195
197
|
Retries with the same recipient, raw bytes, and metadata context reuse the inbound record. Aliases receive distinct membership context. Changing signed metadata produces a distinct checksum: stable values are preferable to per-attempt timestamps or random IDs. Keep downstream document extraction and external side effects idempotent too.
|
|
196
|
-
|
|
197
|
-
## Adopt incrementally
|
|
198
|
-
|
|
199
|
-
1. Inventory existing accepted addresses, organization/subdomain mappings, fallback destinations, and disabled sites. Backfill records explicitly; do not silently create a mailbox for every incoming address.
|
|
200
|
-
2. Confirm existing Worker routing for one organization's exact domain and activate only verified addresses. Exercise an alias, unknown address, suspended mailbox, and cross-tenant destination with synthetic mail.
|
|
201
|
-
3. Add request verification and preserve your current sender review, archive behavior, error responses, and size limits. Test that rejected or held mail cannot reach the processing queue.
|
|
202
|
-
4. Connect site provisioning to the same mailbox API used by the [management engine](management-engine.md). Scope the engine through existing host authentication and authorization.
|
|
203
|
-
5. Retire duplicate infrastructure only after comparing delivery and recovery behavior. Inbound adoption does not require switching your outbound provider.
|
|
204
|
-
|
|
205
|
-
Keep the shared operational communications history separate from tenant-local raw emails and mailbox membership when those serve different purposes. Define who owns deletion, archive retention, deduplication, and processing status; two competing delivery ledgers make recovery harder.
|
|
206
|
-
|
|
207
|
-
The gem's custom-ingress integration test exercises a custom controller against two real SQLite tenant databases, preserving binary MIME bytes and signed metadata before routing jobs run. It also verifies rejected metadata, host review, accepted-address enforcement, and retry behavior. It does not verify a deployed customer's Cloudflare DNS, Worker authentication evidence, R2 archive, or document-processing pipeline; those require that host's end-to-end tests.
|
data/docs/delivery-events.md
CHANGED
|
@@ -118,11 +118,8 @@ existing state, and a nonterminal event cannot replace terminal state. A later
|
|
|
118
118
|
terminal complaint can replace delivery. Invalid timestamps raise. Match records
|
|
119
119
|
and lock recipient state before using this ordering helper.
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
121
|
+
Schedule polling and replay in your application, and choose a retention period
|
|
122
|
+
for delivery receipts.
|
|
126
123
|
|
|
127
124
|
Poll a single batch:
|
|
128
125
|
|
data/docs/features.md
CHANGED
|
@@ -1,138 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Features
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Cloudflare Email handles sending, receiving, and delivery tracking. Mailbox Kit
|
|
4
|
+
adds persistent inboxes and a management UI. Start with the
|
|
5
|
+
[Rails quickstart](getting-started.md).
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
7
|
+
## Sending
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
- Use existing Action Mailer templates or the plain Ruby client.
|
|
10
|
+
- Send text, HTML, attachments, multipart messages, cc/bcc, and replies.
|
|
11
|
+
- Save outgoing messages in a [durable outbox](outbox.md) with a history of attempts and recipient outcomes.
|
|
12
|
+
- Retry jobs using the saved send operation; uncertain sends stay available for review.
|
|
13
|
+
- Track delivered, deferred, bounced, failed, rejected, and complained messages through [delivery events](delivery-events.md).
|
|
14
|
+
- [Match replies](thread-correlation.md) using Cloudflare's returned message ID.
|
|
13
15
|
|
|
14
|
-
|
|
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 |
|
|
16
|
+
## Receiving
|
|
31
17
|
|
|
32
|
-
|
|
18
|
+
- Deploy the included [Cloudflare Worker](../templates/deploy-to-cloudflare/README.md).
|
|
19
|
+
- Retain incoming mail in R2 and retry while Rails is unavailable.
|
|
20
|
+
- Verify signed requests and the actual SMTP recipient before storing email.
|
|
21
|
+
- Keep original messages and attachments in Rails Action Mailbox.
|
|
22
|
+
- Deduplicate repeated deliveries.
|
|
23
|
+
- Process email with ordinary Rails mailbox handlers.
|
|
24
|
+
- Test incoming email locally with `bin/rails cloudflare:email:dev`.
|
|
33
25
|
|
|
34
|
-
|
|
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.
|
|
26
|
+
## Inboxes
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
- Create named [mailboxes](mailboxes.md), aliases, and optional catch-all addresses.
|
|
29
|
+
- Associate mailboxes with application records through `owner_ref`.
|
|
30
|
+
- List unread messages, mark them read, archive them, and retain their originals.
|
|
31
|
+
- Suspend mailboxes while reserving their addresses.
|
|
32
|
+
- Send from an active mailbox through the outbox.
|
|
33
|
+
- Mount a [server-rendered management UI](management-engine.md) using your app's login and permissions.
|
|
34
|
+
- Use [customer subdomains](../templates/worker/docs/domain-setup.md) with Rails address lookup.
|
|
44
35
|
|
|
45
|
-
|
|
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.
|
|
36
|
+
## Storage and providers
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`Mailboxes.with_recipient` offers the same destination context for applications
|
|
54
|
-
that own their storage. Neither replaces your sender policy or business workflow.
|
|
38
|
+
SQLite and PostgreSQL are supported. Database multi-tenancy is **off by default**;
|
|
39
|
+
you can configure [separate SQLite databases](activerecord-tenanted.md) when needed.
|
|
55
40
|
|
|
56
|
-
[
|
|
57
|
-
and
|
|
58
|
-
incomplete inspection; a passing snapshot does not prove live email delivery.
|
|
41
|
+
[Mailbox Kit](../mailbox-kit/README.md) works with other Action Mailbox integrations.
|
|
42
|
+
Incoming and outgoing mail can use different providers.
|
|
59
43
|
|
|
60
|
-
##
|
|
44
|
+
## Administration
|
|
61
45
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
Use [configuration checks and tasks](reference.md#observability-and-permissions),
|
|
47
|
+
[routing diagnostics](routing-diagnostics.md), and ActiveSupport notifications to
|
|
48
|
+
monitor your integration. Custom endpoints can reuse the [ingress verification API](custom-ingress.md).
|
|
49
|
+
[Routing analytics](routing-deliveries.md) can supply additional delivery evidence
|
|
50
|
+
for qualifying messages to verified Routing destinations.
|
|
66
51
|
|
|
67
|
-
|
|
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).
|
|
52
|
+
## What the statuses mean
|
|
71
53
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
| Result | What it tells you |
|
|
54
|
+
| Status | Meaning |
|
|
75
55
|
| --- | --- |
|
|
76
|
-
|
|
|
77
|
-
| `
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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)
|
|
56
|
+
| Send accepted | Cloudflare accepted the send; delivery updates may follow |
|
|
57
|
+
| Outbox `partial` | Recipients have different acceptance outcomes; inspect each recipient |
|
|
58
|
+
| Delivery event `delivered` | The receiving mail server accepted the message |
|
|
59
|
+
| Inbound Rails `delivered` | Your mailbox handler finished processing |
|
|
60
|
+
| Inbox read / archived | Your application's inbox state |
|
|
61
|
+
|
|
62
|
+
Your app supplies users, permissions, sender acceptance rules, and business
|
|
63
|
+
processing. The management UI covers administration and message reading; compose
|
|
64
|
+
screens, conversations, search, and AI workflows belong in your app.
|