cloudflare-email 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -1
  3. data/README.md +265 -388
  4. data/SECURITY.md +51 -0
  5. data/app/controllers/cloudflare/email/ingress_controller.rb +62 -6
  6. data/docs/activerecord-tenanted.md +135 -0
  7. data/docs/architecture.md +95 -0
  8. data/docs/delivery-events.md +177 -0
  9. data/docs/features.md +107 -0
  10. data/docs/getting-started.md +298 -0
  11. data/docs/mailboxes.md +299 -0
  12. data/docs/outbox.md +175 -0
  13. data/docs/tenant-mailboxes-plan.md +149 -0
  14. data/docs/thread-correlation.md +27 -0
  15. data/docs/troubleshooting.md +103 -0
  16. data/docs/upgrading-0.2.md +96 -0
  17. data/docs/verification/2026-09-10-followup.md +49 -0
  18. data/docs/verification/2026-09-10-install-upgrade.md +86 -0
  19. data/docs/verification/2026-09-10-live.md +102 -0
  20. data/docs/verification/2026-09-10.md +175 -0
  21. data/docs/verification/2026-09-11-extraction.md +32 -0
  22. data/docs/verification/2026-09-11-outbound-ledger.md +60 -0
  23. data/docs/verification/2026-09-11-security.md +76 -0
  24. data/docs/verification/2026-09-11-tenant-mailboxes.md +75 -0
  25. data/examples/plain_ruby.rb +24 -0
  26. data/lib/cloudflare/email/active_record/base.rb +48 -0
  27. data/lib/cloudflare/email/active_record/delivery_events.rb +72 -0
  28. data/lib/cloudflare/email/active_record/event_inbox.rb +77 -0
  29. data/lib/cloudflare/email/active_record/event_receipt.rb +17 -0
  30. data/lib/cloudflare/email/active_record/mail_snapshot.rb +24 -0
  31. data/lib/cloudflare/email/active_record/outbound_delivery.rb +22 -0
  32. data/lib/cloudflare/email/active_record/outbound_recipient.rb +13 -0
  33. data/lib/cloudflare/email/active_record/outbound_reconciliation.rb +15 -0
  34. data/lib/cloudflare/email/active_record/outbox.rb +219 -0
  35. data/lib/cloudflare/email/active_record/outbox_notifications.rb +34 -0
  36. data/lib/cloudflare/email/active_record.rb +5 -0
  37. data/lib/cloudflare/email/client.rb +51 -11
  38. data/lib/cloudflare/email/consume_events_task.rb +22 -0
  39. data/lib/cloudflare/email/delivery_event.rb +70 -0
  40. data/lib/cloudflare/email/delivery_method.rb +3 -0
  41. data/lib/cloudflare/email/deploy_worker_task.rb +3 -0
  42. data/lib/cloudflare/email/dev_ingress_guard.rb +27 -0
  43. data/lib/cloudflare/email/dev_tunnel.rb +31 -5
  44. data/lib/cloudflare/email/doctor.rb +16 -38
  45. data/lib/cloudflare/email/endpoint.rb +24 -0
  46. data/lib/cloudflare/email/engine.rb +28 -0
  47. data/lib/cloudflare/email/envelope.rb +58 -0
  48. data/lib/cloudflare/email/event_consumer.rb +86 -0
  49. data/lib/cloudflare/email/mailboxes/configuration.rb +58 -0
  50. data/lib/cloudflare/email/mailboxes/events.rb +112 -0
  51. data/lib/cloudflare/email/mailboxes/inbound_retention.rb +14 -0
  52. data/lib/cloudflare/email/mailboxes/jobs.rb +34 -0
  53. data/lib/cloudflare/email/mailboxes/models.rb +148 -0
  54. data/lib/cloudflare/email/mailboxes/provider_correlation.rb +11 -0
  55. data/lib/cloudflare/email/mailboxes/service.rb +288 -0
  56. data/lib/cloudflare/email/mailboxes/shared_event_receipt.rb +14 -0
  57. data/lib/cloudflare/email/mailboxes.rb +11 -0
  58. data/lib/cloudflare/email/message_id.rb +12 -0
  59. data/lib/cloudflare/email/provision_catchall_task.rb +4 -4
  60. data/lib/cloudflare/email/provision_route_task.rb +2 -1
  61. data/lib/cloudflare/email/replay_events_job.rb +18 -0
  62. data/lib/cloudflare/email/response.rb +21 -3
  63. data/lib/cloudflare/email/routing_provisioner.rb +71 -45
  64. data/lib/cloudflare/email/send_job.rb +41 -0
  65. data/lib/cloudflare/email/send_test.rb +5 -29
  66. data/lib/cloudflare/email/signing.rb +2 -2
  67. data/lib/cloudflare/email/tenancy.rb +78 -0
  68. data/lib/cloudflare/email/tenant_job_context.rb +90 -0
  69. data/lib/cloudflare/email/verification.rb +30 -8
  70. data/lib/cloudflare/email/version.rb +1 -1
  71. data/lib/cloudflare/email/worker_deployer.rb +5 -3
  72. data/lib/cloudflare-email.rb +4 -1
  73. data/lib/generators/cloudflare/email/install_generator.rb +48 -33
  74. data/lib/generators/cloudflare/email/mailboxes/mailboxes_generator.rb +55 -0
  75. data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_mailboxes.rb +54 -0
  76. data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_receiving_domains.rb +16 -0
  77. data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_shared_events.rb +35 -0
  78. data/lib/generators/cloudflare/email/outbox/outbox_generator.rb +28 -0
  79. data/lib/generators/cloudflare/email/outbox/templates/create_cloudflare_email_outbox.rb +48 -0
  80. data/lib/generators/cloudflare/email/templates/initializer.rb +3 -3
  81. data/lib/generators/cloudflare/email/templates/main_mailbox.rb +2 -7
  82. data/lib/generators/cloudflare/email/tracking/templates/create_cloudflare_email_event_receipts.rb +22 -0
  83. data/lib/generators/cloudflare/email/tracking/tracking_generator.rb +26 -0
  84. data/lib/tasks/cloudflare_email.rake +41 -4
  85. data/templates/worker/README.md +61 -11
  86. data/templates/worker/package-lock.json +2627 -0
  87. data/templates/worker/package.json +8 -5
  88. data/templates/worker/scripts/wrangler.mjs +16 -0
  89. data/templates/worker/src/index.js +83 -4
  90. data/templates/worker/test/index.test.ts +117 -5
  91. data/templates/worker/wrangler.toml +13 -4
  92. metadata +107 -6
  93. data/lib/cloudflare/email/secure_message_id.rb +0 -89
@@ -1,37 +1,87 @@
1
1
  # cloudflare-email-ingress
2
2
 
3
3
  A Cloudflare Email Worker that forwards inbound mail to a Rails ActionMailbox
4
- ingress shipped with the [`cloudflare-email`](https://github.com/cole/cloudflare-email)
4
+ ingress shipped with the [`cloudflare-email`](https://github.com/cole-robertson/cloudflare-email)
5
5
  gem.
6
6
 
7
7
  ## Deploy
8
8
 
9
9
  ```sh
10
- npm install
11
- wrangler secret put INGRESS_SECRET # same value as cloudflare.ingress_secret in Rails credentials
12
- wrangler secret put RAILS_INGRESS_URL # e.g. https://your-app.com/rails/action_mailbox/cloudflare/inbound_emails
13
- wrangler deploy
10
+ npm ci
11
+ npx wrangler secret put INGRESS_SECRET --env production
12
+ npx wrangler secret put RAILS_INGRESS_URL --env production
13
+ npm run deploy -- --env production
14
14
  ```
15
15
 
16
+ Use Node 22 (at least 22.12), 24, or 26+. `INGRESS_SECRET` must match Rails' ingress secret;
17
+ `RAILS_INGRESS_URL` is your public HTTPS endpoint, for example
18
+ `https://your-app.com/rails/action_mailbox/cloudflare/inbound_emails`.
19
+ The deploy and dev scripts require an explicit `development`, `staging`, or
20
+ `production` environment. Each has its own Worker and secrets; set both secrets
21
+ for each environment you use. For local development, run
22
+ `npm run dev -- --env development`, with secrets in `.dev.vars.development`.
23
+ Never put secrets into `wrangler.toml` or version control.
24
+
25
+ The Worker refuses non-HTTPS destinations, URL credentials, and fragments.
26
+ HTTP is allowed only for `localhost`, `127.0.0.1`, or `[::1]` local verification.
27
+ The Worker and Rails ingress each default to a 25 MiB MIME size limit. Set
28
+ `MAX_EMAIL_BYTES` to the same positive integer in both environments to change
29
+ that limit. Both check actual body bytes; declared sizes alone are not trusted.
30
+ Rails returns HTTP 413 for oversized mail and rejects malformed or stale signing
31
+ headers before reading the body. Configure request size/rate limits and read
32
+ timeouts at your reverse proxy as well: application limits do not bound earlier
33
+ server buffering or the time spent receiving an HTTP request.
34
+
16
35
  Then in the Cloudflare dashboard:
17
36
 
18
37
  1. **Email Routing → Routes**
19
38
  2. Add a route for the address you want to receive on (e.g. `support@yourdomain.com`)
20
- 3. Action: **Send to a Worker** → `cloudflare-email-ingress`
39
+ 3. Action: **Send to a Worker** → `cloudflare-email-ingress-production`
40
+
41
+ Select the corresponding suffix when deploying another environment. Configure
42
+ Email Routing DNS for the exact domain/subdomain first. Deploying this Worker
43
+ does not change DNS or create routing rules.
21
44
 
22
45
  ## How it works
23
46
 
24
47
  For each inbound message, the Worker:
25
48
 
26
49
  1. Reads the raw RFC822 bytes from `message.raw`.
27
- 2. Computes `HMAC-SHA256(INGRESS_SECRET, "{unix_timestamp}.{raw_body}")`.
50
+ 2. Encodes `{"from": message.from, "to": message.to}` as unpadded base64url JSON,
51
+ then computes `HMAC-SHA256(INGRESS_SECRET, "v2.{unix_timestamp}.{encoded_envelope}.{raw_body}")`.
28
52
  3. POSTs the raw bytes to `RAILS_INGRESS_URL` with:
29
53
  - `Content-Type: message/rfc822`
30
54
  - `X-CF-Email-Timestamp: <unix seconds>`
31
55
  - `X-CF-Email-Signature: <hex digest>`
32
- 4. If the Rails app responds non-2xx, the Worker calls `message.setReject` so
33
- Cloudflare returns a delivery failure to the sender (the message is not
34
- silently dropped).
56
+ - `X-CF-Email-Signature-Version: 2`
57
+ - `X-CF-Email-Envelope: <encoded envelope>`
58
+ 4. If Rails responds non-2xx, the network fails, or the request exceeds 15 seconds,
59
+ the Worker calls `message.setReject`. Redirects are refused to prevent sending
60
+ message content and the signature to a different endpoint. The Worker does
61
+ not retry delivery automatically. A timeout can occur after Rails accepted
62
+ the message, so consumers should handle duplicate deliveries.
35
63
 
36
64
  The Rails controller verifies the signature in constant time and rejects
37
- timestamps older than 5 minutes (replay protection).
65
+ timestamps outside its 5-minute acceptance window. A signature authenticates the
66
+ Worker request, not the original email sender. Captured requests remain valid
67
+ inside that window; Rails deduplicates messages through Action Mailbox.
68
+
69
+ Deploy the Rails gem and this Worker together with ingress paused during the
70
+ transition. Rails requires v2 signatures and rejects v1/missing versions. It stores authenticated
71
+ SMTP metadata separately from MIME; applications read it using
72
+ `Cloudflare::Email::Envelope.for(inbound_email)`. Duplicate detection includes the
73
+ exact SMTP recipient, preserving separate To/Cc/Bcc deliveries. The format accepts
74
+ ASCII dot-atom addresses up to 254 bytes (local part up to 64 bytes), plus an empty
75
+ sender for bounce messages. Other address forms are rejected before forwarding.
76
+
77
+ ## Validate changes
78
+
79
+ ```sh
80
+ npm ci
81
+ npm test
82
+ npm run check
83
+ npm audit
84
+ ```
85
+
86
+ Tooling dependencies are development-only; the deployed Worker imports no npm
87
+ packages. `check` bundles a development deployment locally without publishing.