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,94 @@
|
|
|
1
|
+
# Confirm delivery to verified Routing destinations
|
|
2
|
+
|
|
3
|
+
Cloudflare can deliver mail to a verified Email Routing destination through Routing without producing an Email Sending delivery callback. You can keep using the recipient's normal address. Do not remove its verified destination or rewrite the recipient to obtain a callback.
|
|
4
|
+
|
|
5
|
+
The optional Routing Analytics integration confirms **positive delivery to the receiving mail server** from authenticated Cloudflare analytics. It does not prove inbox placement or that a person read the message. Missing analytics leave the outcome unresolved; they never authorize a resend or provider fallback.
|
|
6
|
+
|
|
7
|
+
## Enable it
|
|
8
|
+
|
|
9
|
+
The plain Ruby client needs no Rails or Active Record dependency:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "cloudflare/email/routing_analytics"
|
|
13
|
+
|
|
14
|
+
client = Cloudflare::Email::RoutingAnalytics::Client.new(
|
|
15
|
+
account_id: ENV.fetch("CLOUDFLARE_ACCOUNT_ID"),
|
|
16
|
+
zone_id: ENV.fetch("CLOUDFLARE_ANALYTICS_ZONE_ID"),
|
|
17
|
+
api_token: ENV.fetch("CLOUDFLARE_ANALYTICS_TOKEN")
|
|
18
|
+
)
|
|
19
|
+
client.verify_account!
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use a separate token with **Zone Analytics Read and Zone Read**, restricted to the sending zone. The client verifies that the zone belongs to the supplied account before querying analytics. Keep sending and queue credentials separate. A token is never written into a receipt.
|
|
23
|
+
|
|
24
|
+
For durable Rails tracking, install the outbox first, then generate the additional receipt table:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
bin/rails generate cloudflare:email:outbox # only if not already installed
|
|
28
|
+
bin/rails generate cloudflare:email:routing_tracking
|
|
29
|
+
bin/rails db:migrate
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Receipts encrypt their JSON evidence using Rails Active Record Encryption. Configure the application's `active_record.encryption.primary_key`, `deterministic_key`, and `key_derivation_salt` before recording receipts (Rails' `db:encryption:init` task generates keys). Keep keys in credentials or protected environment configuration and back them up separately. There is no plaintext fallback enabled by this integration. Receipt metadata includes account and operation identifiers; protect the database and backups as usual.
|
|
33
|
+
|
|
34
|
+
The generated initializer explicitly requires `cloudflare/email/active_record/routing_deliveries`. Installing the gem alone enables no scheduler, network request, tracking table, or tenancy behavior.
|
|
35
|
+
|
|
36
|
+
## Collect and apply evidence
|
|
37
|
+
|
|
38
|
+
Run network requests outside database transactions. The durable outbox must have retained the accepted operation's SMTP envelope and provider Message-ID:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
tracking = Cloudflare::Email::ActiveRecord::RoutingDeliveries
|
|
42
|
+
|
|
43
|
+
client.delivery_evidence(
|
|
44
|
+
message_id: delivery.provider_message_id,
|
|
45
|
+
since: delivery.request_started_at,
|
|
46
|
+
until_time: Time.now.utc
|
|
47
|
+
).each do |evidence|
|
|
48
|
+
receipt = tracking.record(delivery: delivery, evidence: evidence)
|
|
49
|
+
tracking.apply(receipt) do |saved_delivery, recipient|
|
|
50
|
+
# Optional business projection using this same database connection.
|
|
51
|
+
# Read recipient.state; do not send mail or call external services here.
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`record` commits the original evidence before projection. Repeating it returns the same receipt. A changed authentic event under the same identity, or association with another operation, raises an error. Query windows and other rows returned alongside that event may differ between polls without creating duplicate receipts. Client payloads retain the complete original response envelope, provider row, authenticated account/zone, source, and query window and are immutable through ordinary model updates.
|
|
57
|
+
|
|
58
|
+
`apply` returns `:applied`, including successful replay. It updates only a nonterminal recipient and marks the receipt applied in the same transaction. It preserves the accepted operation, envelope snapshot, and every existing terminal webhook fact, including a bounce. Thus `:applied` means evidence was processed; inspect the recipient's actual state. A later genuine Sending complaint continues to update the recipient through the existing Sending event API.
|
|
59
|
+
|
|
60
|
+
Callbacks run only when the recipient changes. Same-database callback errors roll back the recipient, business writes, and receipt completion together; the original pending receipt remains available. A callback must not send email or perform external effects.
|
|
61
|
+
|
|
62
|
+
This is not a webhook endpoint: accept evidence only from the authenticated client or your protected durable storage. Constructing an `Evidence` object validates its shape and correlation, not its provenance. Never deserialize arbitrary incoming request bodies into trusted analytics evidence.
|
|
63
|
+
|
|
64
|
+
## Retry projection fairly
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
result = tracking.replay(account_id: account_id, limit: 100, after_id: saved_cursor)
|
|
68
|
+
result.errors.each do |failure|
|
|
69
|
+
ErrorReporter.report(failure[:error], receipt_id: failure[:receipt_id])
|
|
70
|
+
end
|
|
71
|
+
save_cursor(result.finished ? 0 : result.after_id)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The batch is bounded to 1–1,000 receipts. Failed receipts advance the cursor, allowing later receipts to proceed. After a complete pass, reset the cursor and revisit failures. Errors remain visible in the return value; your application must report them. Supply the same optional projection callback when replaying. Give fresh analytics discovery a separate scheduling opportunity so repeated projection failures cannot starve new evidence. No database transaction is interrupted by an HTTP or polling timeout.
|
|
75
|
+
|
|
76
|
+
The client uses a five-second total network deadline, five-second socket timeouts, a 1,000,000-byte streamed response cap and no implicit HTTP retry. A failed request is visible to your scheduler. Queries returning 100 rows are rejected as potentially truncated; use a narrower historical window and retain existing unresolved records. Plan availability, analytics retention and sampling can limit what Cloudflare can prove.
|
|
77
|
+
|
|
78
|
+
## Deliberately conservative matching
|
|
79
|
+
|
|
80
|
+
The initial API requires exactly one accepted saved SMTP-envelope recipient, exactly one matching accepted operation in the account, matching sender and normalized provider Message-ID, and a nonempty provider session ID. The event must be `newEmail`, `delivered`, final, unsampled and not an NDR. Its timestamp must fall inside the requested historical window and at or after the saved request start, rounded down to Cloudflare's second precision.
|
|
81
|
+
|
|
82
|
+
MIME `To` and analytics `to` are never used as envelope identity. Multi-recipient operations, sampled rows, forwarding rows and missing events do not become guessed delivery or failure. Malformed, truncated, conflicting or wrongly scoped qualifying evidence raises. Terminal delivery can override an equal or later nonterminal observation; existing terminal facts stay intact.
|
|
83
|
+
|
|
84
|
+
## Tenant and cross-database applications
|
|
85
|
+
|
|
86
|
+
Multi-tenancy remains off by default. If enabled, use the existing `Tenancy.with` or `Mailboxes.for_tenant` context for every outbox and Routing receipt operation, and install the receipt migration in each tenant database. Record instances cannot be moved between tenant contexts. A separate analytics account does not select or authorize a tenant.
|
|
87
|
+
|
|
88
|
+
The gem checks provider-ID uniqueness inside the selected database. If one Cloudflare account spans several tenant databases, the host must also enforce account-wide provider-ID uniqueness in its shared directory or ledger before calling `record`. Tenant-local lookup alone cannot establish that a matching operation is unique across independent databases.
|
|
89
|
+
|
|
90
|
+
An application may reserve an explicit physical storage key for system mail, such as `_system_outbound`, using its existing tenant provisioning and backup process. That storage key is not an organization or authorization identity. Keep nil tenant context fail-closed; do not silently map it to shared storage. Reserve and validate the key in the host application so a customer cannot claim it.
|
|
91
|
+
|
|
92
|
+
For business records on another database, keep the host's own durable intake/projection record. Commit the gem evidence and projection in the selected tenant database, then finish the host projection. If the second commit fails, replay the same gem evidence and finish the host work independently. The gem's callback will not run again after its receipt was applied. There is no distributed transaction or exactly-once external effect guarantee.
|
|
93
|
+
|
|
94
|
+
Cloudflare references: [Email event subscriptions](https://developers.cloudflare.com/email-service/platform/event-subscriptions/) and [metrics and analytics](https://developers.cloudflare.com/email-service/observability/metrics-analytics/).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Check whether an address is configured to reach your Worker
|
|
2
|
+
|
|
3
|
+
This diagnostic is available in gem version 0.3.0.
|
|
4
|
+
|
|
5
|
+
A mailbox registered in Rails is not proof that Cloudflare can deliver to it.
|
|
6
|
+
Use this read-only check when onboarding a receiving domain, investigating a
|
|
7
|
+
missing message, or checking an existing catch-all before activating addresses.
|
|
8
|
+
|
|
9
|
+
For wildcard organization subdomains, also use the
|
|
10
|
+
[domain setup guide and public DNS checker](../templates/worker/docs/domain-setup.md).
|
|
11
|
+
This API inspects exact-domain configured records and does not resolve wildcard
|
|
12
|
+
inheritance. Missing exact records can therefore coexist with working inherited
|
|
13
|
+
MX. Public DNS and real delivery checks provide separate evidence; this diagnostic
|
|
14
|
+
is not a requirement to manually onboard every organization in Cloudflare.
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
bin/rails cloudflare:email:check_route \
|
|
18
|
+
ADDRESS=houston@customer.example.com \
|
|
19
|
+
WORKER_NAME=cloudflare-email-ingress-production \
|
|
20
|
+
ACCOUNT_ID=your-cloudflare-account-id
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Configure your Cloudflare management token in the normal gem credentials. The
|
|
24
|
+
inspection needs Zone Read, DNS Read, and Email Routing Read permissions. No Edit
|
|
25
|
+
permissions are needed. `WORKER_NAME` defaults to the gem's environment-scoped
|
|
26
|
+
Worker name; `ACCOUNT_ID` falls back to the configured account, if present.
|
|
27
|
+
|
|
28
|
+
Every check reports `PASS`, `FAIL`, or `UNKNOWN`. The command exits zero only
|
|
29
|
+
when all configuration checks pass. An unavailable API, insufficient permission,
|
|
30
|
+
an unsupported matcher, or ambiguous rule ordering produces `UNKNOWN`, not a
|
|
31
|
+
successful result. Output excludes raw provider responses and credentials.
|
|
32
|
+
|
|
33
|
+
The check inspects:
|
|
34
|
+
|
|
35
|
+
- The closest containing Cloudflare zone and its account when an expected account is supplied.
|
|
36
|
+
- Whether Email Routing is enabled for the zone.
|
|
37
|
+
- MX and SPF records for the **exact receiving domain**. Parent-domain records
|
|
38
|
+
cannot satisfy a subdomain's requirements, and a parent's unrelated MX provider
|
|
39
|
+
does not conflict with correctly configured subdomain records.
|
|
40
|
+
- Active explicit address rules before falling back to the catch-all. An explicit
|
|
41
|
+
drop, forwarding action, or different Worker takes precedence over a correct
|
|
42
|
+
catch-all. Disabled explicit rules are ignored. Multiple matching rules or
|
|
43
|
+
unsupported matcher forms leave selection unverified rather than guessing
|
|
44
|
+
priority or tie behavior.
|
|
45
|
+
|
|
46
|
+
List reads are paginated and bounded to 50 pages per endpoint, responses to 1 MiB,
|
|
47
|
+
and each request to 20 seconds (with shorter connection/read timeouts). An
|
|
48
|
+
incomplete inspection does not report success. This command only issues GET
|
|
49
|
+
requests. It never enables routing, changes DNS, repairs rules, or activates
|
|
50
|
+
mailbox records.
|
|
51
|
+
|
|
52
|
+
## Use from Ruby
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
require "cloudflare/email/routing_diagnostics"
|
|
56
|
+
|
|
57
|
+
report = Cloudflare::Email::RoutingDiagnostics.new(
|
|
58
|
+
api_token: ENV.fetch("CLOUDFLARE_API_TOKEN")
|
|
59
|
+
).check(
|
|
60
|
+
address: "houston@customer.example.com",
|
|
61
|
+
worker_name: "cloudflare-email-ingress-production",
|
|
62
|
+
account_id: ENV.fetch("CLOUDFLARE_ACCOUNT_ID")
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
report[:status] # "pass", "fail", or "unknown"
|
|
66
|
+
report[:checks] # [{ name: "zone", status: "pass", message: "..." }, ...]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This API does not require Active Record or enable multi-tenancy. You can use it
|
|
70
|
+
with a custom Worker and an existing application ingestion pipeline. It reports
|
|
71
|
+
a provider configuration snapshot; it does **not** prove public DNS propagation,
|
|
72
|
+
full SPF evaluation, successful live delivery, the Worker's application URL or
|
|
73
|
+
behavior, or application acceptance. Keep those as separate checks, including a
|
|
74
|
+
controlled end-to-end delivery when appropriate. A passing report does not
|
|
75
|
+
automatically change mailbox readiness or constitute delivery evidence.
|
data/docs/troubleshooting.md
CHANGED
|
@@ -9,7 +9,7 @@ into public logs or issues.
|
|
|
9
9
|
|
|
10
10
|
| What you see | What to check next |
|
|
11
11
|
| --- | --- |
|
|
12
|
-
| New generator or API is missing | Check `Gemfile.lock`.
|
|
12
|
+
| New generator or API is missing | Check `Gemfile.lock`. Use cloudflare-email 0.4.x with mailbox-kit 0.1.x; follow the [installation guide](getting-started.md#install-the-current-code). |
|
|
13
13
|
| Credentials appear to be ignored | Nonempty Rails credentials override environment variables. Check the Rails environment and restart the app after changes. |
|
|
14
14
|
| Authentication or domain error | Run `bin/rails cloudflare:email:doctor`; check account ID, token permissions and sending-domain verification. |
|
|
15
15
|
| `doctor` reports limited read access | A send token may lack diagnostic read permissions. Review the specific result; diagnostics alone cannot prove whether sending works. |
|
|
@@ -30,15 +30,15 @@ FROM=hello@mail.example.com TO=you@example.net bin/rails cloudflare:email:send_t
|
|
|
30
30
|
| --- | --- |
|
|
31
31
|
| Address receives nothing | Verify receiving-subdomain onboarding and the address route's Worker. Sending-domain verification is separate. |
|
|
32
32
|
| Route provisioning refuses a subdomain | Complete Email Routing subdomain setup and DNS preflight first. Do not enable or replace apex routing just to bypass the error. |
|
|
33
|
-
| Worker reports 401 from Rails | Check matching ingress secrets and
|
|
33
|
+
| Worker reports 401 from Rails | Check matching ingress secrets and compatible signature versions. The Worker defaults to v2. The opt-in v3 [custom-ingress API](custom-ingress.md) requires upgrading Rails before enabling Worker metadata. |
|
|
34
34
|
| Worker reports 408 | Check clock accuracy and the five-minute signing window. |
|
|
35
35
|
| Worker reports 413 or rejects size | Raw MIME exceeds the configured limit. Match `MAX_EMAIL_BYTES` on Rails and Worker and check upstream request limits. |
|
|
36
36
|
| Worker reports 3xx | Point it directly at the final HTTPS ingress URL; redirects are intentionally rejected. |
|
|
37
|
-
| Worker times out | Check Rails availability and request latency.
|
|
37
|
+
| Worker times out | Check Rails availability and request latency. Direct mode can reject mail; the default [durable inbound path](../templates/worker/README.md#durable-inbound-delivery) retains it in R2 for retry and scheduled recovery. |
|
|
38
38
|
| Rails returns 200 but your product shows no message | Check ActionMailbox records, job workers, failed routing jobs and your mailbox's `process` method. The default mailbox only logs receipt. |
|
|
39
39
|
| Retrying creates no new inbound record | Identical MIME for the same SMTP recipient is intentionally deduplicated. |
|
|
40
40
|
| Bcc message appears routed to the wrong mailbox | Use `Envelope.for(inbound_email)["to"]` after checking the envelope exists; MIME `To` does not identify every SMTP recipient. |
|
|
41
|
-
| Quoted or internationalized addresses are rejected |
|
|
41
|
+
| Quoted or internationalized addresses are rejected | Both v2 and v3 envelopes support ASCII dot-atom addresses, not every possible email address syntax. |
|
|
42
42
|
|
|
43
43
|
## Local development
|
|
44
44
|
|
|
@@ -85,6 +85,7 @@ the actor you supply but does not authenticate that person.
|
|
|
85
85
|
| What you see | What to check next |
|
|
86
86
|
| --- | --- |
|
|
87
87
|
| Queue stays empty | Check the Email Sending subscription, sending domain, selected event types and a real send from that domain. |
|
|
88
|
+
| A verified Routing destination received mail but Sending feedback is missing | Use optional [Routing delivery confirmation](routing-deliveries.md) with its separate read credential. Missing evidence is unresolved, not proof of failure; keep the normal recipient address. |
|
|
88
89
|
| Polling fails before processing | Check the queue ID, separate Queues Read/Write token, HTTP pull configuration and configured event handler. |
|
|
89
90
|
| Events process once then stop | `consume_events` is a one-batch task. Schedule it to run repeatedly. |
|
|
90
91
|
| Events stay unmatched | Check account, normalized provider message ID and recipient. Schedule replay; a receipt can arrive before the send result is saved. |
|
data/docs/upgrading-0.2.md
CHANGED
|
@@ -26,6 +26,11 @@ Pause test ingress while updating Rails and the deployed Worker together; verify
|
|
|
26
26
|
the matching ingress URL and shared secret before resuming delivery. There is no
|
|
27
27
|
legacy routing mode or supported mixed-version rollout.
|
|
28
28
|
|
|
29
|
+
The 0.3 custom-ingress additions preserve v2 as the default and add
|
|
30
|
+
opt-in v3 for authenticated Worker metadata. That additive upgrade is separate
|
|
31
|
+
from the v1-to-v2 break above: upgrade the Rails receiver before enabling v3 in
|
|
32
|
+
your custom Worker. See [custom ingestion](custom-ingress.md).
|
|
33
|
+
|
|
29
34
|
Rails stores authenticated metadata before enqueueing routing jobs.
|
|
30
35
|
`Cloudflare::Email::Envelope.for(inbound_email)` returns a string-keyed `from`/`to`
|
|
31
36
|
hash for accepted ingress. It returns `nil` for records from another ingress or
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Upgrade from 0.2 to 0.3
|
|
2
|
+
|
|
3
|
+
Update your Gemfile to `gem "cloudflare-email", "~> 0.3.0"`, run
|
|
4
|
+
`bundle update cloudflare-email`, and deploy Rails first. Updating the gem does
|
|
5
|
+
not replace your deployed Worker, change DNS, or enable optional mailbox features.
|
|
6
|
+
If you are upgrading from 0.1, complete the [0.2 upgrade](upgrading-0.2.md) too.
|
|
7
|
+
|
|
8
|
+
## Receiving mail while Rails is down
|
|
9
|
+
|
|
10
|
+
New installations can use the [Deploy to Cloudflare template](../templates/deploy-to-cloudflare/README.md)
|
|
11
|
+
to provision the Worker resources through Cloudflare's setup form. Existing
|
|
12
|
+
installations should preserve their resources and pending mail using the steps below.
|
|
13
|
+
|
|
14
|
+
The bundled Worker now stores inbound mail in private R2 before accepting it,
|
|
15
|
+
then delivers through a Queue with scheduled recovery. Before deploying it:
|
|
16
|
+
|
|
17
|
+
1. Create an R2 bucket and Queue for each environment.
|
|
18
|
+
2. Configure the `INBOUND_EMAIL_STORE` binding, `INBOUND_EMAIL_QUEUE` producer
|
|
19
|
+
and consumer, and once-per-minute cron from the bundled `wrangler.toml`.
|
|
20
|
+
3. Preserve your ingress URL and shared secret. Deploy with Wrangler first;
|
|
21
|
+
the Rails deploy task does not provision infrastructure.
|
|
22
|
+
4. Verify that mail queues during a Rails outage, drains after recovery, and
|
|
23
|
+
retries without duplicating application records. Monitor pending mail and age.
|
|
24
|
+
|
|
25
|
+
Follow the [durable delivery guide](../templates/worker/docs/durable-inbound.md)
|
|
26
|
+
for commands, retention requirements, capacity limits, and recovery behavior.
|
|
27
|
+
Missing infrastructure fails visibly. To deliberately retain single-attempt
|
|
28
|
+
delivery, explicitly select `INBOUND_DELIVERY_MODE=direct` before deploying.
|
|
29
|
+
Direct mode cannot buffer outages. Keep existing bindings and cron so previously
|
|
30
|
+
stored mail continues to drain. The old `DURABLE_INBOUND_ENABLED` flag is removed.
|
|
31
|
+
|
|
32
|
+
Messages without a parseable Message-ID now receive a deterministic fallback
|
|
33
|
+
across Rails hosts. Historical records created with the old hostname-dependent
|
|
34
|
+
fallback may duplicate on replay; reconcile old archives before replaying them.
|
|
35
|
+
Retain deduplication records for your recovery horizon and make app processing
|
|
36
|
+
idempotent. Durable transport is not an exactly-once delivery guarantee.
|
|
37
|
+
|
|
38
|
+
## Optional features
|
|
39
|
+
|
|
40
|
+
- [Management engine](management-engine.md): mount a server-rendered mailbox UI
|
|
41
|
+
with host authentication, scopes, and permissions. It is not mounted automatically.
|
|
42
|
+
- [Mailboxes](mailboxes.md): enable domain catch-all explicitly on an existing
|
|
43
|
+
active address. Follow the catch-all schema upgrade instructions; exact
|
|
44
|
+
registered addresses still take precedence, and sending remains exact-address only.
|
|
45
|
+
- [Custom ingestion](custom-ingress.md): integrate verified envelopes and raw
|
|
46
|
+
persistence into an existing app. Deploy Rails before enabling v3 metadata
|
|
47
|
+
signatures in a custom Worker; ordinary v2 forwarding remains supported.
|
|
48
|
+
- [Routing delivery confirmation](routing-deliveries.md): configure separate
|
|
49
|
+
analytics credentials and durable receipts if needed. API acceptance alone
|
|
50
|
+
is not proof of delivery, and ambiguous evidence must not trigger a resend.
|
|
51
|
+
|
|
52
|
+
Database tenancy remains opt-in. Existing tenant-enabled applications can now
|
|
53
|
+
preload their schemas without selecting a tenant; actual mailbox access still
|
|
54
|
+
requires explicit tenant context. No new database tables are required merely to
|
|
55
|
+
upgrade the gem or use the durable inbound Worker.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Custom ingress and routing diagnostics verification
|
|
2
|
+
|
|
3
|
+
Scope: upstream gem integration tools informed by Rebulk's existing ingestion
|
|
4
|
+
pipeline. This report covers local synthetic verification, not a Rebulk rollout.
|
|
5
|
+
|
|
6
|
+
## Results
|
|
7
|
+
|
|
8
|
+
| Check | Result |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| Full Ruby suite, Ruby 3.4.1 / Rails 7.2, 8.0, 8.1 | Each: 257 tests, 916 assertions, no failures/errors/skips |
|
|
11
|
+
| Shared ingress unit tests | 12 tests, 98 assertions; independent Node/Ruby v3 binary/Unicode vector, tampering, metadata bounds, short streams, immutable results, retry identity |
|
|
12
|
+
| Custom Rails endpoint | 5 integration tests, 37 assertions; separate SQLite tenant databases, policy before persistence, durable signed metadata and retries |
|
|
13
|
+
| Built-in Rails endpoint | V3 persistence before routing enqueue, binary preservation, retries and unsigned v2 metadata rejection; covered in the full suite |
|
|
14
|
+
| Read-only routing diagnostics | 27 tests, 50 assertions; mocked Cloudflare configuration, exact-domain DNS, rule selection, ambiguous/inaccessible responses and pagination |
|
|
15
|
+
| Worker | 45 tests pass; Wrangler dry-run succeeds |
|
|
16
|
+
| Packaged consumer | Isolated plain Ruby install and packaged Rails installation/ingress fixtures pass |
|
|
17
|
+
| Local workerd → Rails | Real local forwarding, Bcc routing, attachment preservation, wrong/missing secrets, redirects and timeout handling pass |
|
|
18
|
+
|
|
19
|
+
Independent reviews found two verifier issues: a readable stream can return
|
|
20
|
+
short chunks, and HMAC must use the timestamp's exact transmitted bytes. Both
|
|
21
|
+
were corrected and covered by regression tests. Freshness still parses decimal
|
|
22
|
+
seconds; existing bundled Worker timestamps are unchanged.
|
|
23
|
+
|
|
24
|
+
## Reproduce
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test
|
|
28
|
+
BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test
|
|
29
|
+
BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bundle exec rake test
|
|
30
|
+
bundle exec ruby script/verify_package.rb
|
|
31
|
+
BUNDLE_GEMFILE=gemfiles/local_ingress.gemfile bundle exec ruby script/verify_local_ingress.rb
|
|
32
|
+
npm --prefix templates/worker test
|
|
33
|
+
npm --prefix templates/worker run check
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use Node 22 for the Worker and local forwarding checks.
|
|
37
|
+
|
|
38
|
+
## Limits
|
|
39
|
+
|
|
40
|
+
No Rebulk application code, live DNS, Worker deployment, inbox activation, R2
|
|
41
|
+
archive, or gem publication was changed. The local workerd test exercises the
|
|
42
|
+
default v2 transport; opt-in v3 is covered by Worker tests, an independent shared
|
|
43
|
+
signature vector, and real Rails integration tests, not deployed Cloudflare mail.
|
|
44
|
+
|
|
45
|
+
Routing diagnostics inspect provider configuration through mocked API tests.
|
|
46
|
+
A passing configuration snapshot does not establish public DNS propagation,
|
|
47
|
+
complete SPF evaluation, Worker application behavior, or live delivery. Signed
|
|
48
|
+
metadata authenticates the forwarding integration's assertions; the host still
|
|
49
|
+
owns original-sender trust, held-message policy, and business processing.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Destination context and email persistence verification
|
|
2
|
+
|
|
3
|
+
Scope: standalone accepted-address resolution and destination context yielded
|
|
4
|
+
from the existing persisted receive path. No Rebulk deployment or gem release.
|
|
5
|
+
|
|
6
|
+
## Results
|
|
7
|
+
|
|
8
|
+
- Full suites on Ruby 3.4.1 with Rails 7.2, 8.0 and 8.1: each passed with
|
|
9
|
+
257 top-level tests and 916 assertions. These include subprocess integration
|
|
10
|
+
fixtures whose internal assertions are not included in that top-level count.
|
|
11
|
+
- The standalone service fixture uses two real SQLite tenant databases with
|
|
12
|
+
colliding mailbox IDs and no ActionMailbox loaded. It verifies host-owned
|
|
13
|
+
binary-email persistence, immutable destinations, aliases, inactive records,
|
|
14
|
+
nested context restoration, host exceptions and missing-tenant refusal without
|
|
15
|
+
creating a database. Lookup itself creates no mailbox membership.
|
|
16
|
+
- The custom Rails ingress fixture verifies saved raw MIME and signed metadata,
|
|
17
|
+
mailbox membership, and a host business link created from the destination.
|
|
18
|
+
Routing jobs execute in the correct tenant after commit. Retries do not
|
|
19
|
+
duplicate the linked records. A host write failure rolls back the email and
|
|
20
|
+
membership and schedules no routing job.
|
|
21
|
+
- Existing callbacks remain supported, including strict zero-argument lambdas.
|
|
22
|
+
- Packaged plain Ruby and Rails consumer checks pass, including mailbox ingress,
|
|
23
|
+
routing and existing retention behavior.
|
|
24
|
+
- Independent review found no remaining blockers. A callback arity compatibility
|
|
25
|
+
issue was fixed and covered before completion.
|
|
26
|
+
|
|
27
|
+
## Reproduce
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test
|
|
31
|
+
BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test
|
|
32
|
+
BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bundle exec rake test
|
|
33
|
+
bundle exec ruby script/verify_package.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Boundaries
|
|
37
|
+
|
|
38
|
+
Destination context is a lifecycle snapshot, not host authorization or a durable
|
|
39
|
+
processing claim. Applications still validate owners and sender policy and make
|
|
40
|
+
downstream work idempotent. The host owns custom-store transactions; `receive`
|
|
41
|
+
preserves the existing shared tenant transaction for ActionMailbox persistence.
|
|
42
|
+
Neither API makes cross-database or external object-store operations atomic.
|
|
43
|
+
|
|
44
|
+
Managed mailbox memberships protect raw email from automatic incineration;
|
|
45
|
+
deliberate purge, backups and host-reference cleanup remain application policy.
|
|
46
|
+
These local tests do not verify Rebulk's actual queues, sender policy or archive.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Action Mailbox composition verification
|
|
2
|
+
|
|
3
|
+
## Independent review follow-up
|
|
4
|
+
|
|
5
|
+
The independent architecture review of `63660eb` confirmed the Rails/core/provider
|
|
6
|
+
split and reproduced a missing inbound membership index with SQLite's query planner.
|
|
7
|
+
The follow-up adds that index to both installers and supplies `mailbox_kit:upgrade`
|
|
8
|
+
for existing schemas. The generator fixture verifies indexed lookup, existing
|
|
9
|
+
membership preservation and repeat application. It passes 4 tests / 27 assertions.
|
|
10
|
+
|
|
11
|
+
Cloudflare configuration now delegates shared validation to the core, and its
|
|
12
|
+
session behavior is an explicit integration module. Handler overrides retain
|
|
13
|
+
their existing extension point. The short core README links to packaged
|
|
14
|
+
integration and upgrade guides.
|
|
15
|
+
|
|
16
|
+
After these changes, the full suite passes 289 tests / 1,151 assertions. Package
|
|
17
|
+
verification passes for Rails-free Cloudflare, packaged ingress/outbound/tenancy,
|
|
18
|
+
and an independent Rails consumer without Cloudflare in its bundle.
|
|
19
|
+
|
|
20
|
+
This is local/package verification, not a Rebulk production rollout. Automatic
|
|
21
|
+
model-owner lifecycle bindings and a universal outbound adapter framework remain
|
|
22
|
+
outside this change. Existing application acceptance, ownership and business
|
|
23
|
+
processing claims must be retained.
|
|
24
|
+
|
|
25
|
+
Mailbox Kit now treats `ActionMailbox::InboundEmail` as the original email record.
|
|
26
|
+
The kit's `Message` remains an inbox membership with read/archive state; no new
|
|
27
|
+
raw-email table, parser, routing job or processing-status model was introduced.
|
|
28
|
+
|
|
29
|
+
## Verified behavior
|
|
30
|
+
|
|
31
|
+
- `Mailboxes.receive(recipient:, source:)` resolves the trusted receiving scope
|
|
32
|
+
before Rails persistence, and attaches the existing record on source replay.
|
|
33
|
+
Identical source in two inboxes of one tenant produces one Rails email, two
|
|
34
|
+
memberships and one routing job. Read state remains independent per inbox.
|
|
35
|
+
- Identical source in different tenants sharing a database produces distinct
|
|
36
|
+
Rails records/jobs. `Session#attach` rejects a row already associated with a
|
|
37
|
+
different tenant, and resolves IDs only inside the selected connection.
|
|
38
|
+
- `Session#attach(recipient:, inbound_email_id:)` supports already-stored Rails
|
|
39
|
+
mail. It is idempotent per mailbox/email, preserves alias reservations and
|
|
40
|
+
refuses suspended addresses or missing records.
|
|
41
|
+
- The stock Rails Postmark ingress was exercised through HTTP in the isolated
|
|
42
|
+
Rails fixture. Duplicate webhook payloads produced one Rails email; explicit
|
|
43
|
+
host-authorized attachment created its inbox membership without reprocessing.
|
|
44
|
+
- Cloudflare retains its prior authenticated envelope/metadata identity and
|
|
45
|
+
stable Message-ID fallback. Its controller uses `receive_into_mailbox!` and
|
|
46
|
+
repairs missing membership on replay without re-enqueueing Rails routing.
|
|
47
|
+
The older `persist_action_mailbox!` still returns nil on duplicate delivery.
|
|
48
|
+
- Rails' `incinerate = false` prevents automatic cleanup scheduling without a
|
|
49
|
+
membership. With normal cleanup enabled, aged processed unassociated mail is
|
|
50
|
+
removed while inbox-associated mail survives. The guard uses Rails' public
|
|
51
|
+
inbound-email load hook and the same row lock as attachment and purge.
|
|
52
|
+
- Removing one of two memberships preserves the shared raw email; removing the
|
|
53
|
+
last through explicit purge removes it. Failed membership persistence rolls
|
|
54
|
+
back Rails database records and prevents a routing enqueue.
|
|
55
|
+
- PostgreSQL deliveries were synchronized at Rails creation to force a unique
|
|
56
|
+
conflict. Both calls completed with one inbound record, one membership, one
|
|
57
|
+
Active Storage blob record and one routing job. A savepoint rollback keeps the
|
|
58
|
+
losing transaction usable before looking up the winning record.
|
|
59
|
+
|
|
60
|
+
## Checks
|
|
61
|
+
|
|
62
|
+
- Full Ruby suite: 289 top-level tests, 1,151 assertions passed. Integration
|
|
63
|
+
subprocesses exercise the additional Rails behavior rather than inflating these
|
|
64
|
+
top-level counts.
|
|
65
|
+
- `script/verify_package.rb`: both gem archives built; Rails-free Cloudflare
|
|
66
|
+
consumer, packaged Cloudflare integration and independent core-only Rails
|
|
67
|
+
consumer passed. The independent bundle contains no Cloudflare gem.
|
|
68
|
+
- PostgreSQL inbound suite: 25 tests, 273 assertions passed on a disposable local
|
|
69
|
+
PostgreSQL 15 instance. CI runs the same suite against PostgreSQL 16 alongside
|
|
70
|
+
the existing outbox tests.
|
|
71
|
+
- SQLite core/compatibility, separate-tenant ingress and custom ingress regression
|
|
72
|
+
suites were exercised locally. CI covers the supported Ruby/Rails matrix.
|
|
73
|
+
|
|
74
|
+
Run the PostgreSQL check only against a disposable database:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
BUNDLE_GEMFILE=gemfiles/postgres.gemfile \
|
|
78
|
+
POSTGRES_TEST_URL=postgres://localhost/cloudflare_email_test \
|
|
79
|
+
bundle exec ruby script/verify_postgres_inbound.rb
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The script creates a unique schema and drops that schema when its fixture exits.
|
|
83
|
+
|
|
84
|
+
## Limits
|
|
85
|
+
|
|
86
|
+
The Postmark test is a fixed, authorized single-database route, not a live
|
|
87
|
+
Postmark deployment or a complete dynamic-tenant provider adapter. The caller
|
|
88
|
+
must authenticate ingress and authorize the envelope recipient and inbound ID;
|
|
89
|
+
MIME To/X-Original-To headers are not tenant entitlement. Tenant selection after
|
|
90
|
+
ingress cannot relocate raw mail already persisted to another database.
|
|
91
|
+
|
|
92
|
+
Raw blob uploads and external side effects are not made transactional by Active
|
|
93
|
+
Record. This check does not establish exactly-once business effects, provider
|
|
94
|
+
outage recovery, lost-job recovery, or live delivery. Existing Cloudflare durable
|
|
95
|
+
transport remains responsible for its outage/retry behavior. Owner lifecycle
|
|
96
|
+
macros and a general outbound adapter framework are separate work.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Durable inbound and Routing delivery verification
|
|
2
|
+
|
|
3
|
+
This change brings the verified Routing-delivery correlation from Rebulk into an
|
|
4
|
+
optional gem API and adds inbound storage/recovery for Rails outages. All tests
|
|
5
|
+
below use synthetic messages and disposable local storage. This report does not
|
|
6
|
+
claim the new Worker configuration is deployed in Rebulk production.
|
|
7
|
+
|
|
8
|
+
## Boundaries verified
|
|
9
|
+
|
|
10
|
+
- Shared sending/queue HTTP requests stream bounded responses, including gzip
|
|
11
|
+
expansion, and enforce a total network deadline. Real socket tests cover
|
|
12
|
+
slow streams, socket cleanup, oversized replies and uncertain queue ACKs.
|
|
13
|
+
No ambiguous failure automatically resends an accepted email.
|
|
14
|
+
- Authenticated Routing analytics validates account/zone, provider ID, sender,
|
|
15
|
+
request window and a single saved envelope recipient. Dedicated encrypted
|
|
16
|
+
receipts preserve provider evidence and support idempotent projection.
|
|
17
|
+
- Inbound R2 persistence finishes before successful Email Worker return. Queue
|
|
18
|
+
messages contain only a storage pointer. Failed enqueue, Rails errors, lost
|
|
19
|
+
responses, failed deletion and exhausted retries retain the pending payload.
|
|
20
|
+
Scheduled pagination advances over poison messages and retries retained mail.
|
|
21
|
+
- A real local workerd instance uses R2 and Queues against Rails with SQLite and
|
|
22
|
+
disk-backed Active Storage. With queue retries disabled, Rails 503 leaves mail
|
|
23
|
+
in R2; invoking the scheduled handler after recovery persists one message and
|
|
24
|
+
removes the pending object. Returning 503 after Rails commits also replays to
|
|
25
|
+
one record. MIME and binary attachments remain byte-identical.
|
|
26
|
+
- Missing Message-ID fallback remains stable when Rails hostnames change. Valid
|
|
27
|
+
Message-IDs and raw MIME are preserved. Pre-upgrade fallback IDs require care
|
|
28
|
+
when replaying historical archive records.
|
|
29
|
+
- Local workerd capacity checks pass for one 25 MiB message and two concurrent
|
|
30
|
+
25 MiB receipts, followed by concurrent handoffs. The receiver verifies exact
|
|
31
|
+
SHA256 and HMAC values, and R2 pending objects clear after acknowledgement.
|
|
32
|
+
Local workerd does not certify production memory-limit enforcement.
|
|
33
|
+
|
|
34
|
+
## Reproduce
|
|
35
|
+
|
|
36
|
+
Use Ruby 3.4 and Node 22 or another supported version:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
BUNDLE_GEMFILE=gemfiles/local_ingress.gemfile bundle install
|
|
40
|
+
npm ci --prefix templates/worker
|
|
41
|
+
BUNDLE_GEMFILE=gemfiles/local_ingress.gemfile bundle exec rake test
|
|
42
|
+
npm test --prefix templates/worker
|
|
43
|
+
npm run check --prefix templates/worker
|
|
44
|
+
node script/verify_durable_capacity.mjs
|
|
45
|
+
BUNDLE_GEMFILE=gemfiles/local_ingress.gemfile bundle exec ruby script/verify_local_ingress.rb
|
|
46
|
+
BUNDLE_GEMFILE=gemfiles/local_ingress.gemfile bundle exec ruby script/verify_package.rb
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The local runtime harness invokes the real scheduled handler through a temporary
|
|
50
|
+
test-only endpoint. It does not test Cloudflare's production cron scheduler or
|
|
51
|
+
SMTP delivery behavior. No test endpoint is shipped in the Worker.
|
|
52
|
+
|
|
53
|
+
## Before production enablement
|
|
54
|
+
|
|
55
|
+
Provision a private R2 bucket, queue and schedule per environment, and keep
|
|
56
|
+
pending objects free of lifecycle expiration. Deploy the updated Rails ingress
|
|
57
|
+
first. Verify maximum message size and recovery throughput with expected traffic,
|
|
58
|
+
then perform a staging outage drill through the actual Cloudflare email route.
|
|
59
|
+
Monitor storage errors and pending object age as well as queue backlog. Keep
|
|
60
|
+
Rails deduplication records throughout the recovery horizon and monitor mailbox
|
|
61
|
+
routing jobs after durable ingestion.
|
|
62
|
+
|
|
63
|
+
Routing tracking is separately opt-in and needs encryption keys, its migration,
|
|
64
|
+
a restricted analytics read credential, and host scheduling. Cross-database
|
|
65
|
+
applications must retain their durable business projection boundary and enforce
|
|
66
|
+
provider-ID uniqueness across tenant databases. Missing delivery evidence stays
|
|
67
|
+
unresolved; it never authorizes another send.
|
|
68
|
+
|
|
69
|
+
R2 storage and retry protect mail after a successful storage commit. They cannot
|
|
70
|
+
promise absolute losslessness during upstream storage failure, deletion,
|
|
71
|
+
misconfiguration or account suspension. See the [operating guide](../../templates/worker/docs/durable-inbound.md).
|
|
@@ -1,48 +1,10 @@
|
|
|
1
|
-
require "active_record"
|
|
2
1
|
require "cloudflare/email/tenancy"
|
|
3
|
-
|
|
2
|
+
require "mailbox_kit/active_record/base"
|
|
4
3
|
module Cloudflare
|
|
5
4
|
module Email
|
|
6
5
|
module ActiveRecord
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
self.abstract_class = true
|
|
10
|
-
|
|
11
|
-
class << self
|
|
12
|
-
def connection_pool
|
|
13
|
-
Tenancy.require_context! if Tenancy.enabled?
|
|
14
|
-
super
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
before_validation :verify_cloudflare_email_tenant!
|
|
19
|
-
before_save :verify_cloudflare_email_tenant!
|
|
20
|
-
before_destroy :verify_cloudflare_email_tenant!
|
|
21
|
-
|
|
22
|
-
# These methods can bypass callbacks or load a different row with the same ID.
|
|
23
|
-
%i[reload update_columns delete touch increment! decrement! association].each do |method_name|
|
|
24
|
-
define_method(method_name) do |*args, **kwargs, &block|
|
|
25
|
-
verify_cloudflare_email_tenant!
|
|
26
|
-
super(*args, **kwargs, &block)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
# Both new construction and persisted-row instantiation call this before
|
|
33
|
-
# assigning inverse associations (which precede after_initialize).
|
|
34
|
-
def init_internals
|
|
35
|
-
super
|
|
36
|
-
@cloudflare_email_tenant_key = Tenancy.require_context! if Tenancy.enabled?
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def verify_cloudflare_email_tenant!
|
|
40
|
-
return unless Tenancy.enabled?
|
|
41
|
-
unless @cloudflare_email_tenant_key == Tenancy.require_context!
|
|
42
|
-
raise ConfigurationError, "record belongs to a different tenant context"
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
6
|
+
Base = MailboxKit::ActiveRecord::Base
|
|
7
|
+
TenantConnectionUnavailable = MailboxKit::ActiveRecord::TenantConnectionUnavailable
|
|
46
8
|
end
|
|
47
9
|
end
|
|
48
10
|
end
|