cloudflare-email 0.2.0 → 0.4.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 +109 -0
- data/README.md +44 -9
- data/app/controllers/cloudflare/email/ingress_controller.rb +15 -60
- data/app/controllers/cloudflare/email/management/mailboxes_controller.rb +9 -0
- data/app/controllers/cloudflare/email/management/styles_controller.rb +9 -0
- data/docs/activerecord-tenanted.md +46 -0
- data/docs/architecture.md +13 -3
- data/docs/custom-ingress.md +211 -0
- data/docs/delivery-events.md +9 -0
- data/docs/features.md +36 -5
- data/docs/getting-started.md +19 -8
- data/docs/mailboxes.md +74 -3
- data/docs/management-engine.md +190 -0
- data/docs/routing-deliveries.md +94 -0
- data/docs/routing-diagnostics.md +75 -0
- data/docs/troubleshooting.md +5 -4
- data/docs/upgrading-0.2.md +5 -0
- data/docs/upgrading-0.3.md +55 -0
- data/docs/verification/2026-09-11-custom-ingress.md +49 -0
- data/docs/verification/2026-09-12-destination-context.md +46 -0
- data/docs/verification/2026-09-13-action-mailbox-core.md +96 -0
- data/docs/verification/2026-09-13-durable-inbound-and-routing.md +71 -0
- data/lib/cloudflare/email/active_record/base.rb +3 -41
- data/lib/cloudflare/email/active_record/routing_deliveries.rb +128 -0
- data/lib/cloudflare/email/check_route_task.rb +21 -0
- data/lib/cloudflare/email/client.rb +45 -10
- data/lib/cloudflare/email/delivery_method.rb +2 -0
- data/lib/cloudflare/email/deploy_worker_task.rb +3 -3
- data/lib/cloudflare/email/engine.rb +6 -13
- data/lib/cloudflare/email/envelope.rb +4 -14
- data/lib/cloudflare/email/error.rb +5 -12
- data/lib/cloudflare/email/event_consumer.rb +12 -3
- data/lib/cloudflare/email/ingress.rb +126 -0
- 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 -130
- data/lib/cloudflare/email/mailboxes/service.rb +7 -174
- data/lib/cloudflare/email/mailboxes.rb +1 -0
- data/lib/cloudflare/email/management/adapter.rb +9 -0
- data/lib/cloudflare/email/management/configuration.rb +10 -0
- data/lib/cloudflare/email/management/engine.rb +17 -0
- data/lib/cloudflare/email/management/routes.rb +14 -0
- data/lib/cloudflare/email/management.rb +7 -0
- data/lib/cloudflare/email/provider_metadata.rb +77 -0
- data/lib/cloudflare/email/routing_analytics.rb +196 -0
- data/lib/cloudflare/email/routing_diagnostics.rb +206 -0
- data/lib/cloudflare/email/tenancy.rb +2 -73
- data/lib/cloudflare/email/tenant_job_context.rb +2 -85
- data/lib/cloudflare/email/verification.rb +17 -9
- data/lib/cloudflare/email/version.rb +1 -1
- data/lib/cloudflare/email/worker_deployer.rb +36 -5
- data/lib/cloudflare-email.rb +6 -0
- data/lib/generators/cloudflare/email/install_generator.rb +6 -5
- 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/templates/create_cloudflare_email_mailboxes.rb +5 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_receiving_domains.rb +1 -1
- 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/tasks/cloudflare_email.rake +2 -1
- 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 +104 -0
- data/templates/deploy-to-cloudflare/docs/domain-setup.md +198 -0
- data/templates/deploy-to-cloudflare/package-lock.json +2627 -0
- data/templates/deploy-to-cloudflare/package.json +37 -0
- data/templates/deploy-to-cloudflare/scripts/check-subdomains.mjs +54 -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/test/subdomains.test.ts +51 -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 +186 -15
- data/templates/worker/docs/domain-setup.md +198 -0
- data/templates/worker/docs/durable-inbound.md +149 -0
- data/templates/worker/package-lock.json +2 -2
- data/templates/worker/package.json +2 -1
- data/templates/worker/scripts/check-subdomains.mjs +54 -0
- data/templates/worker/src/index.js +331 -38
- data/templates/worker/test/durable.test.ts +193 -0
- data/templates/worker/test/index.test.ts +78 -20
- data/templates/worker/test/relay.test.ts +226 -0
- data/templates/worker/test/subdomains.test.ts +51 -0
- data/templates/worker/wrangler.toml +50 -0
- metadata +65 -3
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Use your existing inbound email pipeline
|
|
2
|
+
|
|
3
|
+
This integration API is available in gem version 0.3.0.
|
|
4
|
+
|
|
5
|
+
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
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## Register addresses before accepting mail
|
|
10
|
+
|
|
11
|
+
Follow [mailbox setup](mailboxes.md) to register and activate the receiving domain and accepted addresses. With existing catch-all Worker routing, address creation can be a database operation once an administrator has confirmed that routing covers the exact domain. Registering a domain does not create DNS records or extend wildcard coverage.
|
|
12
|
+
|
|
13
|
+
Use the same provisioning service from organization/site onboarding and from the management UI. Keep a stable site reference in the mailbox's `owner_ref`; keep site permissions, slug changes, document workflows, and sender allowlists in your app. Treat a rename as an explicit alias/lifecycle decision so old addresses are not accidentally lost.
|
|
14
|
+
|
|
15
|
+
For separate organization databases, configure [the SQLite tenant adapter](activerecord-tenanted.md) before loading mailbox models. The shared directory resolves the signed recipient to a tenant. Action Mailbox, Active Storage, and the gem's mailbox records must all use that tenant's connection.
|
|
16
|
+
|
|
17
|
+
## Verify a custom endpoint before writing anything
|
|
18
|
+
|
|
19
|
+
`Cloudflare::Email::Ingress.verify` is usable outside the bundled controller. It accepts a String or IO body, bounds the raw body size, and returns an immutable result. Successful verification exposes the exact binary body and authenticated envelope; rejected requests expose no verified message.
|
|
20
|
+
|
|
21
|
+
The following controller is a starting point for a host with registered mailboxes:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "cloudflare/email/ingress"
|
|
25
|
+
|
|
26
|
+
class InboundEmailsController < ActionController::API
|
|
27
|
+
def create
|
|
28
|
+
result = Cloudflare::Email::Ingress.verify(
|
|
29
|
+
secret: Rails.application.credentials.dig(:cloudflare, :ingress_secret),
|
|
30
|
+
headers: request.headers,
|
|
31
|
+
body: request.body,
|
|
32
|
+
content_length: request.content_length,
|
|
33
|
+
max_email_bytes: 25 * 1024 * 1024
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
case result.status
|
|
37
|
+
when :too_large
|
|
38
|
+
return head :payload_too_large
|
|
39
|
+
when :bad_signature, :stale
|
|
40
|
+
return head :unauthorized
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
verified = result.message
|
|
44
|
+
|
|
45
|
+
# If no custom acceptance/processing record is needed, this is sufficient:
|
|
46
|
+
# verified.receive_into_mailbox!
|
|
47
|
+
# It also repairs missing membership on duplicate delivery without rerouting.
|
|
48
|
+
|
|
49
|
+
Cloudflare::Email::Mailboxes.receive(
|
|
50
|
+
recipient: verified.envelope.fetch("to")
|
|
51
|
+
) do |destination|
|
|
52
|
+
# This block runs in the recipient's tenant. Apply your sender/review
|
|
53
|
+
# policy here, before persistence schedules Action Mailbox routing.
|
|
54
|
+
# Your policy may raise a host error or return nil instead of persisting.
|
|
55
|
+
# destination.owner_ref identifies your site/team; validate it using
|
|
56
|
+
# your application's policy in this tenant context.
|
|
57
|
+
verified.persist_action_mailbox!
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
head :ok
|
|
61
|
+
rescue Cloudflare::Email::Mailboxes::Unavailable
|
|
62
|
+
head :unprocessable_entity
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Mount this controller at your own route and point your existing Worker there. The host owns response codes and retry policy. If your policy returns nil and you acknowledge the request, you are accepting responsibility for that email: persist a durable review record or deliberately discard it according to your application's policy. A successful verification alone does not queue processing or create a mailbox.
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
`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.
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
## Persist the email and link your business record
|
|
76
|
+
|
|
77
|
+
Email persistence remains part of the gem's supported path. Action Mailbox stores
|
|
78
|
+
the raw MIME using Active Storage, the verifier saves authenticated metadata, and
|
|
79
|
+
`Mailboxes.receive` adds the inbox membership. Your processing/review record can
|
|
80
|
+
refer to that email rather than storing a second copy of its raw content:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
Cloudflare::Email::Mailboxes.receive(
|
|
84
|
+
recipient: verified.envelope.fetch("to")
|
|
85
|
+
) do |destination|
|
|
86
|
+
# Validate owner_ref and sender policy here, before any routing is scheduled.
|
|
87
|
+
inbound = verified.persist_action_mailbox!
|
|
88
|
+
if inbound
|
|
89
|
+
# Example host model, on the SAME tenant connection as the gem records.
|
|
90
|
+
EmailIntake.create!(
|
|
91
|
+
inbound_email_id: inbound.id,
|
|
92
|
+
mailbox_id: destination.mailbox_id,
|
|
93
|
+
owner_ref: destination.owner_ref
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
inbound # Return the ActionMailbox record; duplicate deliveries return nil.
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The database writes commit together. A host failure rolls back the new email,
|
|
101
|
+
membership and host link and prevents the routing job from being enqueued. This
|
|
102
|
+
does not make an R2 archive, another database, or external API calls transactional.
|
|
103
|
+
Host exceptions propagate; only unavailable registry destinations are translated
|
|
104
|
+
to `Mailboxes::Unavailable`. Existing blocks without a destination argument still
|
|
105
|
+
work.
|
|
106
|
+
|
|
107
|
+
Persisting through ActionMailbox schedules its normal routing after commit. Put
|
|
108
|
+
your processing in that route or a deliberately coordinated job pipeline. If an
|
|
109
|
+
email must be held, implement a routing policy that cannot process it before
|
|
110
|
+
approval; storing it is not itself a review gate. A routing job failure also needs
|
|
111
|
+
normal job retry/recovery. Receipt deduplication does not make every downstream
|
|
112
|
+
business side effect idempotent.
|
|
113
|
+
|
|
114
|
+
Managed mailbox membership protects raw email from ActionMailbox's normal
|
|
115
|
+
automatic incineration. Archiving a mailbox message keeps its content; deliberate
|
|
116
|
+
purging can remove it once no other membership references the email. Raw email
|
|
117
|
+
saved without membership follows the normal ActionMailbox lifecycle. Keep host
|
|
118
|
+
references, backup and R2 retention consistent with your chosen deletion policy.
|
|
119
|
+
Multi-tenancy remains opt-in.
|
|
120
|
+
|
|
121
|
+
## Resolve an address with your own persistence
|
|
122
|
+
|
|
123
|
+
If your application already owns its raw-email store, use the standalone API:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Cloudflare::Email::Mailboxes.with_recipient(
|
|
127
|
+
recipient: verified.envelope.fetch("to")
|
|
128
|
+
) do |destination|
|
|
129
|
+
# Example host-owned persistence service; it must commit durably before ACK.
|
|
130
|
+
ExistingEmailStore.save!(
|
|
131
|
+
tenant_key: destination.tenant_key,
|
|
132
|
+
mailbox_id: destination.mailbox_id,
|
|
133
|
+
recipient: destination.recipient,
|
|
134
|
+
raw: verified.body,
|
|
135
|
+
metadata: verified.storage_metadata
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This checks the active domain, address and mailbox, enters the configured tenant,
|
|
141
|
+
and yields an immutable `Destination` with `tenant_key`, `mailbox_id`, `address_id`,
|
|
142
|
+
`receiving_domain_id`, `recipient` and `owner_ref`. It returns the block's result,
|
|
143
|
+
creates no email or membership, starts no storage transaction, and requires no
|
|
144
|
+
ActionMailbox. Configure the optional mailbox registry and tenant adapter as usual.
|
|
145
|
+
|
|
146
|
+
Both APIs must be called after complete request verification. Destination values
|
|
147
|
+
are routing snapshots, not authorization tokens: an owner reference does not
|
|
148
|
+
prove a site still exists or that a sender may submit to it. Use records inside
|
|
149
|
+
their tenant context and recheck lifecycle/permissions when later work requires
|
|
150
|
+
it. Both APIs restore the previous tenant context when the block returns or raises.
|
|
151
|
+
|
|
152
|
+
## Carry Worker metadata with authenticated provenance
|
|
153
|
+
|
|
154
|
+
The default Worker protocol remains v2: it authenticates the raw message and SMTP envelope. Optional v3 also authenticates a bounded metadata object:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"source": "cloudflare",
|
|
159
|
+
"data": {
|
|
160
|
+
"archive_key": "inbound/example.eml"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The `archive_key` above is an illustrative application-defined value. The gem does not fetch that object or verify an archive exists. Your trusted Worker constructs the data from sources whose provenance you understand.
|
|
166
|
+
|
|
167
|
+
V3 binds the encoded metadata to the timestamp, envelope, and exact raw bytes. The `X-CF-Email-Metadata` header is unpadded base64url UTF-8 JSON. Metadata attached to a v2 request is rejected rather than treated as authenticated. Existing v2 requests without metadata continue to work.
|
|
168
|
+
|
|
169
|
+
The Worker template exports `signedEmailHeaders` for an existing transport and `forwardEmail` for the bundled forwarding behavior. Keep your current archive and retry handling, and use the header builder to authenticate the original raw bytes:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
import { signedEmailHeaders } from "./cloudflare-ingress.js";
|
|
173
|
+
|
|
174
|
+
const headers = await signedEmailHeaders({
|
|
175
|
+
secret: env.INGRESS_SECRET,
|
|
176
|
+
raw, // Uint8Array from your bounded reader
|
|
177
|
+
from: message.from,
|
|
178
|
+
to: message.to,
|
|
179
|
+
metadata: { source: "cloudflare", data: { archive_key: archiveKey } },
|
|
180
|
+
});
|
|
181
|
+
// Send exactly raw with these headers using your existing transport.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
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. Upgrade the Rails receiver before enabling v3 in the Worker.
|
|
185
|
+
|
|
186
|
+
In your Action Mailbox handler, read persisted metadata through the gem:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
require "cloudflare/email/provider_metadata"
|
|
190
|
+
|
|
191
|
+
metadata = Cloudflare::Email::ProviderMetadata.for(inbound_email)
|
|
192
|
+
archive_key = metadata&.dig("data", "archive_key")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This reads verified Active Storage metadata, not similarly named MIME headers. Authenticate your Worker first and preserve the verified object unchanged. Avoid adding secrets or unnecessary personal information to provider metadata.
|
|
196
|
+
|
|
197
|
+
**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.
|
|
198
|
+
|
|
199
|
+
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.
|
|
200
|
+
|
|
201
|
+
## Adopt incrementally
|
|
202
|
+
|
|
203
|
+
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.
|
|
204
|
+
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.
|
|
205
|
+
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.
|
|
206
|
+
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.
|
|
207
|
+
5. Retire duplicate infrastructure only after comparing delivery and recovery behavior. Inbound adoption does not require switching your outbound provider.
|
|
208
|
+
|
|
209
|
+
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.
|
|
210
|
+
|
|
211
|
+
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
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Cloudflare Email Sending can publish outbound lifecycle events to Queues. This is separate from inbound Email Routing and the ActionMailbox ingress.
|
|
4
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
|
+
|
|
5
14
|
## Provision once
|
|
6
15
|
|
|
7
16
|
1. Create a dedicated Cloudflare Queue for your application's sending events.
|
data/docs/features.md
CHANGED
|
@@ -5,7 +5,7 @@ services. You can use just the sending client, add incoming mail, or build a
|
|
|
5
5
|
mailbox on top of the optional database-backed delivery tools.
|
|
6
6
|
|
|
7
7
|
Start with [Getting started](getting-started.md) for working examples. These
|
|
8
|
-
features are available in **0.
|
|
8
|
+
features are available in **0.4.0**, which includes **Mailbox Kit 0.1.0**. Database multi-tenancy is **off by default**:
|
|
9
9
|
the optional mailbox module works in one database unless you explicitly configure
|
|
10
10
|
a tenant connection adapter. A mailbox tenant key alone does not switch databases.
|
|
11
11
|
|
|
@@ -29,6 +29,34 @@ a tenant connection adapter. A mailbox tenant key alone does not switch database
|
|
|
29
29
|
| Observe email processing | ActiveSupport notifications + `doctor` | Send/ingress/event/outbox instrumentation and configuration diagnostics |
|
|
30
30
|
| Deploy receiving infrastructure | Ruby deployer and routing tasks | Environment-specific Workers, ingress secrets, address routes and DNS preflight checks |
|
|
31
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
|
+
|
|
32
60
|
## SQLite works
|
|
33
61
|
|
|
34
62
|
The optional receipt and outbox tables work with **SQLite**. Keep your existing
|
|
@@ -58,7 +86,7 @@ subscriptions. The [getting-started guide](getting-started.md) walks through eac
|
|
|
58
86
|
|
|
59
87
|
## Protections included
|
|
60
88
|
|
|
61
|
-
|
|
89
|
+
By default, incoming requests use v2 HMAC signatures covering the timestamp, SMTP sender,
|
|
62
90
|
SMTP recipient and raw message. Rails checks a five-minute timestamp window.
|
|
63
91
|
Identical MIME retried for the same recipient is deduplicated; delivery to a
|
|
64
92
|
different recipient is stored separately.
|
|
@@ -83,7 +111,9 @@ See [managed mailbox setup](mailboxes.md) for the application-facing API and
|
|
|
83
111
|
The gem supplies email infrastructure and optional managed mailbox records.
|
|
84
112
|
Your app supplies users, mailbox permissions, conversation records, custom folders, search, compose screens,
|
|
85
113
|
drafts and any AI review or approval workflow. It also decides unsubscribe and
|
|
86
|
-
recipient eligibility policy. The
|
|
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.
|
|
87
117
|
|
|
88
118
|
An authenticated SMTP envelope tells your app which address Cloudflare received
|
|
89
119
|
the message for. It does not prove the human sender's identity. Reply correlation
|
|
@@ -91,8 +121,9 @@ also does not authorize access to a conversation. Apply your app's permissions
|
|
|
91
121
|
before displaying mail or sending a reply.
|
|
92
122
|
|
|
93
123
|
You configure Cloudflare domains/DNS, queue subscriptions, durable jobs,
|
|
94
|
-
monitoring, storage protection and retention.
|
|
95
|
-
|
|
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)
|
|
96
127
|
for the full boundary and [security guidance](../SECURITY.md) for deployment.
|
|
97
128
|
|
|
98
129
|
## Where to go next
|
data/docs/getting-started.md
CHANGED
|
@@ -18,11 +18,12 @@ not need to be the same domain as your Rails application's web address.
|
|
|
18
18
|
|
|
19
19
|
### Install the current code
|
|
20
20
|
|
|
21
|
-
Add version 0.
|
|
22
|
-
[upgrade guide](upgrading
|
|
21
|
+
Add version 0.4 to your app's `Gemfile`. Existing mailbox installations should read
|
|
22
|
+
the [core upgrade guide](../mailbox-kit/docs/upgrading.md). When upgrading from 0.2,
|
|
23
|
+
also read the [Worker upgrade guide](upgrading-0.3.md):
|
|
23
24
|
|
|
24
25
|
```ruby
|
|
25
|
-
gem "cloudflare-email", "~> 0.
|
|
26
|
+
gem "cloudflare-email", "~> 0.4.0"
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
```sh
|
|
@@ -123,8 +124,18 @@ In Cloudflare **Email Routing → your apex domain → Settings → Subdomains**
|
|
|
123
124
|
your receiving subdomain, such as `in.example.com`, and finish its DNS setup.
|
|
124
125
|
This is separate from verifying the sending domain.
|
|
125
126
|
|
|
126
|
-
Deploy Rails with ActionMailbox storage and workers configured.
|
|
127
|
-
|
|
127
|
+
Deploy Rails with ActionMailbox storage and workers configured. For guided
|
|
128
|
+
Worker setup, use the [Deploy to Cloudflare template](../templates/deploy-to-cloudflare/README.md).
|
|
129
|
+
It provisions R2 and Queues and prompts for your Rails URL and shared secret.
|
|
130
|
+
Then select that Worker in your Email Routing rule as described in its guide.
|
|
131
|
+
|
|
132
|
+
For the CLI setup instead, run these commands in an environment with production
|
|
133
|
+
configuration and deployment credentials:
|
|
134
|
+
|
|
135
|
+
First create the private R2 bucket and Queue, then deploy the generated Wrangler
|
|
136
|
+
configuration using the [durable setup guide](../templates/worker/docs/durable-inbound.md).
|
|
137
|
+
The Ruby task below updates that provisioned Worker while preserving its bindings.
|
|
138
|
+
It refuses a missing bucket/queue binding or recovery schedule before changing code.
|
|
128
139
|
|
|
129
140
|
```sh
|
|
130
141
|
RAILS_ENV=production bin/rails cloudflare:email:deploy_worker URL=https://app.example.com/rails/action_mailbox/cloudflare/inbound_emails
|
|
@@ -166,11 +177,11 @@ Install `cloudflared`, configure development credentials and a separate developm
|
|
|
166
177
|
receiving route, then start Rails. In another terminal:
|
|
167
178
|
|
|
168
179
|
```sh
|
|
169
|
-
bin/rails cloudflare:email:deploy_worker
|
|
180
|
+
INBOUND_DELIVERY_MODE=direct bin/rails cloudflare:email:deploy_worker
|
|
170
181
|
bin/rails cloudflare:email:dev
|
|
171
182
|
```
|
|
172
183
|
|
|
173
|
-
The first command creates the development Worker; the second points it at a
|
|
184
|
+
The first command creates the development Worker with the explicit direct fallback; the second points it at a
|
|
174
185
|
temporary tunnel to your local Rails server on port 3000. Keep the tunnel running
|
|
175
186
|
while testing and stop it with Ctrl-C. Set `PORT=...` if Rails uses another port.
|
|
176
187
|
The task checks that Rails has the ingress-only guard; restart Rails if asked.
|
|
@@ -295,4 +306,4 @@ override it. Cloudflare has separate [outgoing size and recipient limits](https:
|
|
|
295
306
|
|
|
296
307
|
Use [troubleshooting](troubleshooting.md) when something does not arrive,
|
|
297
308
|
[the feature overview](features.md) to explore more, and
|
|
298
|
-
[the upgrade guide](upgrading-0.
|
|
309
|
+
[the upgrade guide](upgrading-0.3.md) for an existing 0.1 installation.
|
data/docs/mailboxes.md
CHANGED
|
@@ -5,7 +5,7 @@ mail into them, track read/archive state, and send through the durable outbox.
|
|
|
5
5
|
It works in one SQLite database or with a separate database per organization.
|
|
6
6
|
It supplies models and services; your app supplies permissions and the UI.
|
|
7
7
|
|
|
8
|
-
This module is available
|
|
8
|
+
This module is available since version 0.2.0. Install `gem "cloudflare-email", "~> 0.4.0"` for the latest features, including the provider-neutral Mailbox Kit core.
|
|
9
9
|
|
|
10
10
|
**Database multi-tenancy is off by default.** The mailbox generator works with
|
|
11
11
|
one ordinary database. Calling `for_tenant` groups and scopes mailbox records;
|
|
@@ -34,7 +34,8 @@ require "cloudflare/email/mailboxes"
|
|
|
34
34
|
Loading this module enables mailbox lookup on the gem's Cloudflare ingress.
|
|
35
35
|
Existing receiving addresses must be registered and activated before switching
|
|
36
36
|
an existing application over. Unregistered or suspended destinations return
|
|
37
|
-
HTTP 422 and the Worker rejects the delivery
|
|
37
|
+
HTTP 422 and the Worker rejects the delivery by default. You can explicitly
|
|
38
|
+
configure a domain catch-all below when your application needs that behavior.
|
|
38
39
|
|
|
39
40
|
For separate organization databases, follow the
|
|
40
41
|
[activerecord-tenanted setup](activerecord-tenanted.md) **before loading the
|
|
@@ -44,6 +45,13 @@ use the same tenant connection as the mailbox tables for atomic incoming storage
|
|
|
44
45
|
|
|
45
46
|
## Register an organization's domain
|
|
46
47
|
|
|
48
|
+
For addresses such as `invoices@acme.in.example.com`, follow the
|
|
49
|
+
[Cloudflare domain setup guide](../templates/worker/docs/domain-setup.md).
|
|
50
|
+
It covers Rebulk's existing wildcard receiving pattern, one-time infrastructure
|
|
51
|
+
verification, and exact organization-domain registration in Rails. The gem does
|
|
52
|
+
not require a per-organization Worker allowlist or Cloudflare approval once that
|
|
53
|
+
receiving infrastructure is established; new accounts must verify the provider behavior.
|
|
54
|
+
|
|
47
55
|
Run directory management from your authorized administration/provisioning code.
|
|
48
56
|
Do not expose arbitrary domain claims to customers without ownership checks.
|
|
49
57
|
|
|
@@ -146,6 +154,67 @@ are explicit: the module does not automatically strip `+tags` or invent address
|
|
|
146
154
|
fallbacks. Reserve application-specific names such as Rebulk's `tracking` in
|
|
147
155
|
your mailbox-management policy before creation.
|
|
148
156
|
|
|
157
|
+
## Receive unregistered local parts with an optional catch-all
|
|
158
|
+
|
|
159
|
+
Catch-all receiving is **off by default** and is available in the 0.3
|
|
160
|
+
version. It lets one existing address receive otherwise unregistered addresses
|
|
161
|
+
on its exact domain, without creating an alias row for each incoming local part.
|
|
162
|
+
For example, `anything@acme.example.com` can arrive in an existing support mailbox.
|
|
163
|
+
It does not cover subdomains such as `anything@other.acme.example.com`.
|
|
164
|
+
|
|
165
|
+
Fresh installations include the schema. For an existing installation, generate
|
|
166
|
+
the optional upgrade and apply it to the database containing your mailbox tables:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
bin/rails generate cloudflare:email:mailboxes:catch_all
|
|
170
|
+
bin/rails db:migrate
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
With separate tenant databases, pass
|
|
174
|
+
`--tenant-migrations-path=db/tenant_migrate` and run your application's tenant
|
|
175
|
+
migration command for every affected tenant. Updating the gem alone keeps older
|
|
176
|
+
schemas working with exact-address routing. Enabling a catch-all without its
|
|
177
|
+
migration raises a configuration error.
|
|
178
|
+
|
|
179
|
+
First configure and verify the domain's catch-all Email Routing rule in Cloudflare
|
|
180
|
+
to send mail to your Worker. Then use your authorized provisioning code:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
Cloudflare::Email::Mailboxes.for_tenant("organization-123") do |account|
|
|
184
|
+
mailbox = account.mailboxes.find_by!(owner_ref: "team:42")
|
|
185
|
+
address = account.addresses(mailbox.id).find_by!(address: "support@acme.example.com")
|
|
186
|
+
account.enable_catch_all(address.id,
|
|
187
|
+
evidence: "Domain catch-all Worker route verified in setup ticket 57")
|
|
188
|
+
|
|
189
|
+
# Later, stop accepting unregistered local parts:
|
|
190
|
+
# account.disable_catch_all(address.id)
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The address, mailbox and receiving domain must be active before enabling it.
|
|
195
|
+
The evidence is an operator assertion, as with ordinary address activation;
|
|
196
|
+
this call does not change Cloudflare rules. Only one active address per domain
|
|
197
|
+
can serve as a catch-all. A database constraint also protects concurrent changes.
|
|
198
|
+
Suspending that address pauses fallback. If another catch-all is enabled while
|
|
199
|
+
it is suspended, disable the old flag before reactivating the old address.
|
|
200
|
+
|
|
201
|
+
Exact registered addresses always take precedence. An exact address that is
|
|
202
|
+
pending, suspended, or belongs to a suspended mailbox is rejected; it does not
|
|
203
|
+
fall through to another mailbox. The catch-all also stops receiving when its
|
|
204
|
+
mailbox or domain is suspended.
|
|
205
|
+
|
|
206
|
+
Both `with_recipient` and `receive` yield `destination.catch_all == true` only
|
|
207
|
+
when fallback was used. `destination.recipient` remains the actual envelope
|
|
208
|
+
recipient, while `address_id` identifies the existing backing address. Stored
|
|
209
|
+
mailbox membership retains that actual recipient too. Apps can use this flag to
|
|
210
|
+
enforce additional rules, such as allowing fallback only while an organization
|
|
211
|
+
has one site, before accepting/persisting the message.
|
|
212
|
+
|
|
213
|
+
Catch-all receiving grants **no additional sending addresses**. Outbound From,
|
|
214
|
+
Sender and envelope sender must still match an exact active registered address.
|
|
215
|
+
Disable catch-all receiving through the API; the management UI displays its
|
|
216
|
+
configuration but does not enable or disable it.
|
|
217
|
+
|
|
149
218
|
## Read incoming mail
|
|
150
219
|
|
|
151
220
|
The Worker signs the SMTP recipient. Rails verifies the whole request, resolves
|
|
@@ -294,6 +363,8 @@ lookup. Drain existing ActionMailbox/ActiveStorage queues before enabling this
|
|
|
294
363
|
mode: previously serialized jobs without the new metadata are rejected.
|
|
295
364
|
|
|
296
365
|
The gem does not create organizations/users, verify customer domain ownership,
|
|
297
|
-
provide IMAP/POP, or supply
|
|
366
|
+
provide IMAP/POP, or supply a full compose/conversation inbox. The optional
|
|
367
|
+
[management engine](management-engine.md) supplies mailbox administration and
|
|
368
|
+
plain-text previews using your host's access policy. The original Rebulk document-processing
|
|
298
369
|
Worker contract and sender-review policy still need a separate application
|
|
299
370
|
migration; installing this module does not replace that live pipeline.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Optional mailbox management engine
|
|
2
|
+
|
|
3
|
+
The management engine is a small, server-rendered Rails interface for the
|
|
4
|
+
[managed mailbox APIs](mailboxes.md). It works with ordinary Rails apps and with
|
|
5
|
+
apps using Inertia, React, Turbo or another frontend. No JavaScript, Node build,
|
|
6
|
+
asset pipeline or frontend package is required for the engine.
|
|
7
|
+
|
|
8
|
+
This feature is available in RubyGems 0.3.0.
|
|
9
|
+
|
|
10
|
+
## What you get
|
|
11
|
+
|
|
12
|
+
- Mailbox lists, names, creation and aliases on allowed receiving domains.
|
|
13
|
+
- Address setup status and mailbox suspension/resumption.
|
|
14
|
+
- A paginated list of received messages and an escaped plain-text preview.
|
|
15
|
+
- Optional read/unread and archive/unarchive controls.
|
|
16
|
+
- Ordinary CSRF-protected forms, mobile layouts and bundled CSS.
|
|
17
|
+
|
|
18
|
+
Your app supplies login, permissions, domain entitlements and any organization
|
|
19
|
+
mapping. The engine does not create users, claim domains, edit DNS, provision
|
|
20
|
+
Cloudflare routes, send mail, or permanently delete records. The default creation
|
|
21
|
+
hooks create pending addresses. Your administrator or existing provisioning code
|
|
22
|
+
completes routing and activation through the same Ruby APIs.
|
|
23
|
+
|
|
24
|
+
The viewer never renders sender HTML, loads remote images, or provides raw MIME
|
|
25
|
+
or attachment downloads. It shows attachment names and up to 100,000 plain-text
|
|
26
|
+
characters. Compose, conversations, AI workflows and product search belong in
|
|
27
|
+
your application.
|
|
28
|
+
|
|
29
|
+
## Install and mount
|
|
30
|
+
|
|
31
|
+
First install the managed mailbox tables and enable the module using
|
|
32
|
+
[the mailbox setup guide](mailboxes.md). SQLite works; database tenancy remains
|
|
33
|
+
off unless your app configures it explicitly.
|
|
34
|
+
|
|
35
|
+
Require the engine **before Rails initializes**, after Bundler loads the gem:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# config/application.rb, after Bundler.require(*Rails.groups)
|
|
39
|
+
require "cloudflare/email/management"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Configure a host adapter in an initializer:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# config/initializers/cloudflare_email_management.rb
|
|
46
|
+
Cloudflare::Email::Management.configure do |config|
|
|
47
|
+
# Resolve the class when handling the request, so Rails reloading works.
|
|
48
|
+
config.adapter = ->(controller) { EmailManagementAccess.new(controller) }
|
|
49
|
+
config.back_path = ->(controller) { controller.main_app.root_path }
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Mount it wherever it fits your application:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# config/routes.rb
|
|
57
|
+
mount Cloudflare::Email::Management::Engine => "/email-management",
|
|
58
|
+
as: :email_management
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Installing the gem does not expose this interface automatically. Without a
|
|
62
|
+
configured adapter and enabled mailbox module the mounted interface returns 503.
|
|
63
|
+
The base adapter grants no access. Its public stylesheet endpoint serves only
|
|
64
|
+
the bundled CSS.
|
|
65
|
+
|
|
66
|
+
## Connect your authentication and ownership
|
|
67
|
+
|
|
68
|
+
Here is a single-database example using Rails' generated authentication convention
|
|
69
|
+
(`Session`, `Current.session`, and a signed `session_id` cookie). Match the cookie
|
|
70
|
+
and session lookup to your app; the agentic inbox example uses `session_token`.
|
|
71
|
+
Use the host authentication system's expiry, revocation and access policy.
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# app/services/email_management_access.rb
|
|
75
|
+
class EmailManagementAccess < Cloudflare::Email::Management::Adapter
|
|
76
|
+
ACTIONS = %i[index show create add_address suspend resume
|
|
77
|
+
show_message mark_read archive].freeze
|
|
78
|
+
|
|
79
|
+
def authenticate!
|
|
80
|
+
Current.session = Session.find_by(
|
|
81
|
+
id: controller.request.cookie_jar.signed[:session_id]
|
|
82
|
+
)
|
|
83
|
+
@user = Current.user
|
|
84
|
+
return true if @user
|
|
85
|
+
|
|
86
|
+
controller.redirect_to controller.main_app.new_session_path
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def tenant_key = "application"
|
|
91
|
+
def owner_ref = "User:#{@user.id}"
|
|
92
|
+
|
|
93
|
+
def mailboxes(session)
|
|
94
|
+
session.mailboxes.where(owner_ref: owner_ref)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def allowed?(action, mailbox = nil)
|
|
98
|
+
@user.present? && ACTIONS.include?(action) &&
|
|
99
|
+
(mailbox.nil? || mailbox.owner_ref == owner_ref)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def domains(_session)
|
|
103
|
+
["in.example.com"] # Only domains this user is entitled to use.
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def create_mailbox(session, name:, address:)
|
|
107
|
+
session.create(name: name, address: address, owner_ref: owner_ref)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For Devise, the authentication hook can use your configured Warden scope instead
|
|
113
|
+
of the cookie lookup, for example
|
|
114
|
+
`@user = controller.request.env["warden"]&.authenticate(scope: :user)`.
|
|
115
|
+
The engine inherits from `ActionController::Base`; host `ApplicationController`
|
|
116
|
+
callbacks and methods are not inherited. Make authentication explicit in the
|
|
117
|
+
adapter and return exactly `true` on success, or redirect to the host's login.
|
|
118
|
+
|
|
119
|
+
The directory must already contain the active receiving domain assigned to the
|
|
120
|
+
same registry key. The engine intersects `domains` with that directory and
|
|
121
|
+
intersects the host mailbox relation with the current gem tenant. Parameters
|
|
122
|
+
cannot select an owner or tenant.
|
|
123
|
+
|
|
124
|
+
Do not return every domain merely because it exists in the database. A customer
|
|
125
|
+
app should apply organization entitlement, reserved-name and signup policies.
|
|
126
|
+
For separate tenant databases, return a trusted host-resolved key from
|
|
127
|
+
`tenant_key` and configure the existing gem tenancy adapter and framework
|
|
128
|
+
connections before mailbox models load.
|
|
129
|
+
|
|
130
|
+
## Customize behavior without forking the pages
|
|
131
|
+
|
|
132
|
+
The adapter has these hooks:
|
|
133
|
+
|
|
134
|
+
| Hook | Contract |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `authenticate!` | Authenticate the request; return exactly true or redirect/deny. |
|
|
137
|
+
| `tenant_key` | Trusted registry key; never use an unchecked request parameter. |
|
|
138
|
+
| `mailboxes(session)` | ActiveRecord relation containing mailboxes accessible to this principal. |
|
|
139
|
+
| `allowed?(action, mailbox = nil)` | Return exactly true to permit an action. |
|
|
140
|
+
| `domains(session)` | Array of domain strings permitted for this principal. |
|
|
141
|
+
| `create_mailbox(session, name:, address:)` | Return the created gem mailbox, visible in the host's scope. |
|
|
142
|
+
| `add_address(session, mailbox, address:)` | Add an address through your existing app provisioning service if needed. |
|
|
143
|
+
|
|
144
|
+
The adapter factory, authentication and tenant-key resolution run before the
|
|
145
|
+
mailbox context is entered; the host must establish any context its authentication
|
|
146
|
+
needs. Mailbox/domain scopes, permissions and mutation hooks run inside the
|
|
147
|
+
resolved mailbox context. Creation also runs in a
|
|
148
|
+
database transaction; a returned mailbox outside the host scope rolls back.
|
|
149
|
+
Keep network work out of creation hooks and enqueue provisioning after commit.
|
|
150
|
+
The default alias hook calls `session.add_address`.
|
|
151
|
+
|
|
152
|
+
Return false for `:create` or lifecycle actions to provide a restricted dashboard.
|
|
153
|
+
Use `:show_message` to control message access independently of metadata. Deny
|
|
154
|
+
`:mark_read` and `:archive` when the host product already owns a different
|
|
155
|
+
conversation-level read/folder model. Hidden controls also reject direct requests.
|
|
156
|
+
|
|
157
|
+
An application with its own mailbox record can override creation and alias hooks
|
|
158
|
+
to call that application's service and return the linked gem model. The agentic
|
|
159
|
+
inbox example does this: both UIs and automatic account provisioning share one
|
|
160
|
+
service, so engine-created mailboxes also appear in its full inbox.
|
|
161
|
+
|
|
162
|
+
## Inertia, React and Turbo applications
|
|
163
|
+
|
|
164
|
+
Use a normal HTML link or full-page navigation to the mount. For example:
|
|
165
|
+
|
|
166
|
+
```jsx
|
|
167
|
+
<a href="/email-management">Manage mailboxes</a>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The engine returns HTML, not Inertia responses. Its separate layout keeps host
|
|
171
|
+
frontend dependencies out of the gem. The optional `back_path` callback returns
|
|
172
|
+
a local absolute path for a Back to app link. Programmatic provisioning continues
|
|
173
|
+
to work whether this engine is mounted or not.
|
|
174
|
+
|
|
175
|
+
## Operational behavior
|
|
176
|
+
|
|
177
|
+
Suspension preserves mailbox records and retained mail while disabling mailbox
|
|
178
|
+
sending/receiving. Resuming does not activate pending addresses or repair DNS.
|
|
179
|
+
Read/archive state belongs to the gem mailbox memberships and does not implicitly
|
|
180
|
+
rewrite host conversation records.
|
|
181
|
+
|
|
182
|
+
Private pages use no-store caching, a restrictive CSP and a same-origin referrer
|
|
183
|
+
policy. Forms check both Rails CSRF tokens and origins. Serve the host over HTTPS,
|
|
184
|
+
configure trusted proxy headers correctly, and keep its session cookies secure.
|
|
185
|
+
Do not disable CSRF to work around a proxy configuration issue.
|
|
186
|
+
|
|
187
|
+
Verification includes real Rails sessions, owner/tenant scoping, all operation
|
|
188
|
+
permissions, CSRF/origin checks, rollback, escaped previews, retained raw source,
|
|
189
|
+
and eager-loading a core-only app without exposing the engine. The example also
|
|
190
|
+
tests desktop/mobile Chromium and JavaScript-disabled forms with synthetic mail.
|