cloudflare-email 0.1.0 → 0.3.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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +149 -1
  3. data/README.md +287 -385
  4. data/SECURITY.md +51 -0
  5. data/app/controllers/cloudflare/email/ingress_controller.rb +33 -21
  6. data/app/controllers/cloudflare/email/management/mailboxes_controller.rb +173 -0
  7. data/app/controllers/cloudflare/email/management/styles_controller.rb +13 -0
  8. data/app/views/cloudflare/email/management/mailboxes/index.html.erb +65 -0
  9. data/app/views/cloudflare/email/management/mailboxes/message.html.erb +34 -0
  10. data/app/views/cloudflare/email/management/mailboxes/show.html.erb +86 -0
  11. data/app/views/layouts/cloudflare/email/management.html.erb +29 -0
  12. data/docs/activerecord-tenanted.md +181 -0
  13. data/docs/architecture.md +105 -0
  14. data/docs/custom-ingress.md +207 -0
  15. data/docs/delivery-events.md +186 -0
  16. data/docs/features.md +138 -0
  17. data/docs/getting-started.md +308 -0
  18. data/docs/mailboxes.md +363 -0
  19. data/docs/management-engine.md +190 -0
  20. data/docs/outbox.md +175 -0
  21. data/docs/routing-deliveries.md +94 -0
  22. data/docs/routing-diagnostics.md +68 -0
  23. data/docs/tenant-mailboxes-plan.md +149 -0
  24. data/docs/thread-correlation.md +27 -0
  25. data/docs/troubleshooting.md +104 -0
  26. data/docs/upgrading-0.2.md +101 -0
  27. data/docs/upgrading-0.3.md +55 -0
  28. data/docs/verification/2026-09-10-followup.md +49 -0
  29. data/docs/verification/2026-09-10-install-upgrade.md +86 -0
  30. data/docs/verification/2026-09-10-live.md +102 -0
  31. data/docs/verification/2026-09-10.md +175 -0
  32. data/docs/verification/2026-09-11-custom-ingress.md +49 -0
  33. data/docs/verification/2026-09-11-extraction.md +32 -0
  34. data/docs/verification/2026-09-11-outbound-ledger.md +60 -0
  35. data/docs/verification/2026-09-11-security.md +76 -0
  36. data/docs/verification/2026-09-11-tenant-mailboxes.md +75 -0
  37. data/docs/verification/2026-09-12-destination-context.md +46 -0
  38. data/docs/verification/2026-09-13-durable-inbound-and-routing.md +71 -0
  39. data/examples/plain_ruby.rb +24 -0
  40. data/lib/cloudflare/email/active_record/base.rb +60 -0
  41. data/lib/cloudflare/email/active_record/delivery_events.rb +72 -0
  42. data/lib/cloudflare/email/active_record/event_inbox.rb +77 -0
  43. data/lib/cloudflare/email/active_record/event_receipt.rb +17 -0
  44. data/lib/cloudflare/email/active_record/mail_snapshot.rb +24 -0
  45. data/lib/cloudflare/email/active_record/outbound_delivery.rb +22 -0
  46. data/lib/cloudflare/email/active_record/outbound_recipient.rb +13 -0
  47. data/lib/cloudflare/email/active_record/outbound_reconciliation.rb +15 -0
  48. data/lib/cloudflare/email/active_record/outbox.rb +219 -0
  49. data/lib/cloudflare/email/active_record/outbox_notifications.rb +34 -0
  50. data/lib/cloudflare/email/active_record/routing_deliveries.rb +128 -0
  51. data/lib/cloudflare/email/active_record.rb +5 -0
  52. data/lib/cloudflare/email/check_route_task.rb +21 -0
  53. data/lib/cloudflare/email/client.rb +93 -18
  54. data/lib/cloudflare/email/consume_events_task.rb +22 -0
  55. data/lib/cloudflare/email/delivery_event.rb +70 -0
  56. data/lib/cloudflare/email/delivery_method.rb +5 -0
  57. data/lib/cloudflare/email/deploy_worker_task.rb +6 -3
  58. data/lib/cloudflare/email/dev_ingress_guard.rb +27 -0
  59. data/lib/cloudflare/email/dev_tunnel.rb +31 -5
  60. data/lib/cloudflare/email/doctor.rb +16 -38
  61. data/lib/cloudflare/email/endpoint.rb +24 -0
  62. data/lib/cloudflare/email/engine.rb +33 -0
  63. data/lib/cloudflare/email/envelope.rb +58 -0
  64. data/lib/cloudflare/email/event_consumer.rb +95 -0
  65. data/lib/cloudflare/email/ingress.rb +115 -0
  66. data/lib/cloudflare/email/mailboxes/configuration.rb +58 -0
  67. data/lib/cloudflare/email/mailboxes/events.rb +112 -0
  68. data/lib/cloudflare/email/mailboxes/inbound_retention.rb +14 -0
  69. data/lib/cloudflare/email/mailboxes/jobs.rb +34 -0
  70. data/lib/cloudflare/email/mailboxes/models.rb +165 -0
  71. data/lib/cloudflare/email/mailboxes/provider_correlation.rb +11 -0
  72. data/lib/cloudflare/email/mailboxes/service.rb +372 -0
  73. data/lib/cloudflare/email/mailboxes/shared_event_receipt.rb +14 -0
  74. data/lib/cloudflare/email/mailboxes.rb +11 -0
  75. data/lib/cloudflare/email/management/adapter.rb +31 -0
  76. data/lib/cloudflare/email/management/configuration.rb +17 -0
  77. data/lib/cloudflare/email/management/engine.rb +15 -0
  78. data/lib/cloudflare/email/management/management.css +68 -0
  79. data/lib/cloudflare/email/management/routes.rb +14 -0
  80. data/lib/cloudflare/email/management.rb +7 -0
  81. data/lib/cloudflare/email/message_id.rb +12 -0
  82. data/lib/cloudflare/email/provider_metadata.rb +77 -0
  83. data/lib/cloudflare/email/provision_catchall_task.rb +4 -4
  84. data/lib/cloudflare/email/provision_route_task.rb +2 -1
  85. data/lib/cloudflare/email/replay_events_job.rb +18 -0
  86. data/lib/cloudflare/email/response.rb +21 -3
  87. data/lib/cloudflare/email/routing_analytics.rb +196 -0
  88. data/lib/cloudflare/email/routing_diagnostics.rb +206 -0
  89. data/lib/cloudflare/email/routing_provisioner.rb +71 -45
  90. data/lib/cloudflare/email/send_job.rb +41 -0
  91. data/lib/cloudflare/email/send_test.rb +5 -29
  92. data/lib/cloudflare/email/signing.rb +2 -2
  93. data/lib/cloudflare/email/tenancy.rb +78 -0
  94. data/lib/cloudflare/email/tenant_job_context.rb +90 -0
  95. data/lib/cloudflare/email/verification.rb +38 -8
  96. data/lib/cloudflare/email/version.rb +1 -1
  97. data/lib/cloudflare/email/worker_deployer.rb +41 -8
  98. data/lib/cloudflare-email.rb +4 -1
  99. data/lib/generators/cloudflare/email/install_generator.rb +53 -37
  100. data/lib/generators/cloudflare/email/mailboxes/catch_all/catch_all_generator.rb +21 -0
  101. data/lib/generators/cloudflare/email/mailboxes/catch_all/templates/add_cloudflare_email_catch_all.rb +18 -0
  102. data/lib/generators/cloudflare/email/mailboxes/mailboxes_generator.rb +55 -0
  103. data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_mailboxes.rb +58 -0
  104. data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_receiving_domains.rb +16 -0
  105. data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_shared_events.rb +35 -0
  106. data/lib/generators/cloudflare/email/outbox/outbox_generator.rb +28 -0
  107. data/lib/generators/cloudflare/email/outbox/templates/create_cloudflare_email_outbox.rb +48 -0
  108. data/lib/generators/cloudflare/email/routing_tracking/routing_tracking_generator.rb +28 -0
  109. data/lib/generators/cloudflare/email/routing_tracking/templates/create_cloudflare_email_routing_delivery_receipts.rb +22 -0
  110. data/lib/generators/cloudflare/email/templates/initializer.rb +3 -3
  111. data/lib/generators/cloudflare/email/templates/main_mailbox.rb +2 -7
  112. data/lib/generators/cloudflare/email/tracking/templates/create_cloudflare_email_event_receipts.rb +22 -0
  113. data/lib/generators/cloudflare/email/tracking/tracking_generator.rb +26 -0
  114. data/lib/tasks/cloudflare_email.rake +42 -4
  115. data/lib/tasks/cloudflare_email_diagnostics.rake +11 -0
  116. data/templates/deploy-to-cloudflare/.dev.vars.example +3 -0
  117. data/templates/deploy-to-cloudflare/.gitignore +6 -0
  118. data/templates/deploy-to-cloudflare/LICENSE.txt +21 -0
  119. data/templates/deploy-to-cloudflare/README.md +84 -0
  120. data/templates/deploy-to-cloudflare/package-lock.json +2627 -0
  121. data/templates/deploy-to-cloudflare/package.json +36 -0
  122. data/templates/deploy-to-cloudflare/src/index.js +445 -0
  123. data/templates/deploy-to-cloudflare/test/durable.test.ts +193 -0
  124. data/templates/deploy-to-cloudflare/test/index.test.ts +309 -0
  125. data/templates/deploy-to-cloudflare/test/relay.test.ts +226 -0
  126. data/templates/deploy-to-cloudflare/vitest.config.ts +8 -0
  127. data/templates/deploy-to-cloudflare/wrangler.toml +22 -0
  128. data/templates/worker/README.md +231 -16
  129. data/templates/worker/docs/durable-inbound.md +149 -0
  130. data/templates/worker/package-lock.json +2627 -0
  131. data/templates/worker/package.json +8 -5
  132. data/templates/worker/scripts/wrangler.mjs +16 -0
  133. data/templates/worker/src/index.js +397 -25
  134. data/templates/worker/test/durable.test.ts +193 -0
  135. data/templates/worker/test/index.test.ts +183 -13
  136. data/templates/worker/test/relay.test.ts +226 -0
  137. data/templates/worker/wrangler.toml +63 -4
  138. metadata +154 -7
  139. data/lib/cloudflare/email/secure_message_id.rb +0 -89
@@ -6,16 +6,17 @@ namespace :cloudflare do
6
6
  exit Cloudflare::Email::Doctor.call
7
7
  end
8
8
 
9
- desc "Send a test email via the current Cloudflare Email config (TO=addr FROM=addr)"
9
+ desc "Send a real test email (both FROM=addr and TO=addr are required)"
10
10
  task send_test: :environment do
11
11
  require "cloudflare/email/send_test"
12
12
  exit Cloudflare::Email::SendTest.call(to: ENV["TO"], from: ENV["FROM"])
13
13
  end
14
14
 
15
- desc "Deploy the Worker via Cloudflare API (no wrangler/Node required). URL=https://... sets RAILS_INGRESS_URL"
15
+ desc "Deploy the Worker via Cloudflare API. URL=https://... sets RAILS_INGRESS_URL; SCRIPT=path overrides the Worker source"
16
16
  task deploy_worker: :environment do
17
17
  require "cloudflare/email/deploy_worker_task"
18
- exit Cloudflare::Email::DeployWorkerTask.call(ingress_url: ENV["URL"])
18
+ exit Cloudflare::Email::DeployWorkerTask.call(ingress_url: ENV["URL"], script_path: ENV["SCRIPT"],
19
+ delivery_mode: ENV.fetch("INBOUND_DELIVERY_MODE", "durable"))
19
20
  end
20
21
 
21
22
  desc "Create Cloudflare Email Routing rule: ADDRESS=addr@domain → env-scoped Worker (WORKER=name to override)"
@@ -27,7 +28,7 @@ namespace :cloudflare do
27
28
  )
28
29
  end
29
30
 
30
- desc "Point the zone's catch-all rule at the env-scoped Worker (DOMAIN=in.example.com [WORKER=name])"
31
+ desc "Point a zone-wide catch-all at the env-scoped Worker (DOMAIN=example.com [WORKER=name])"
31
32
  task provision_catchall: :environment do
32
33
  require "cloudflare/email/provision_catchall_task"
33
34
  exit Cloudflare::Email::ProvisionCatchallTask.call(
@@ -41,5 +42,42 @@ namespace :cloudflare do
41
42
  require "cloudflare/email/dev_tunnel"
42
43
  Cloudflare::Email::DevTunnel.call(port: ENV.fetch("PORT", "3000").to_i)
43
44
  end
45
+
46
+ desc "Process one batch of outbound delivery events using config.x.cloudflare_email.event_handler"
47
+ task consume_events: :environment do
48
+ require "cloudflare/email/consume_events_task"
49
+ settings = Rails.application.config.x.cloudflare_email
50
+ exit Cloudflare::Email::ConsumeEventsTask.call(
51
+ handler: settings.event_handler,
52
+ domains: settings.event_domains,
53
+ batch_size: Integer(ENV.fetch("BATCH_SIZE", "5"), 10),
54
+ )
55
+ end
56
+
57
+ desc "Send a prepared durable operation using OPERATION_KEY (no new message is generated)"
58
+ task deliver: :environment do
59
+ require "cloudflare/email/send_job"
60
+ delivery = Cloudflare::Email::SendJob.perform_now(
61
+ Cloudflare::Email::Credentials.account_id, ENV.fetch("OPERATION_KEY"))
62
+ puts "operation=#{delivery.operation_key} state=#{delivery.state}"
63
+ end
64
+
65
+ desc "Replay durable outbound receipts for the configured account (optional MESSAGE_ID)"
66
+ task replay_events: :environment do
67
+ require "cloudflare/email/replay_events_job"
68
+ count = Cloudflare::Email::ReplayEventsJob.perform_now(
69
+ Cloudflare::Email::Credentials.account_id, ENV["MESSAGE_ID"])
70
+ puts "Replayed #{count} receipt(s)."
71
+ end
72
+
73
+ desc "List prepared or uncertain outbound operations requiring dispatch or operator review"
74
+ task pending_deliveries: :environment do
75
+ require "cloudflare/email/active_record"
76
+ Cloudflare::Email::ActiveRecord::OutboundDelivery.where(
77
+ account_id: Cloudflare::Email::Credentials.account_id,
78
+ state: %w[prepared sending unknown partial]).find_each do |delivery|
79
+ puts "operation=#{delivery.operation_key} state=#{delivery.state} updated_at=#{delivery.updated_at.iso8601}"
80
+ end
81
+ end
44
82
  end
45
83
  end
@@ -0,0 +1,11 @@
1
+ namespace :cloudflare do
2
+ namespace :email do
3
+ desc "Inspect routing without changes (ADDRESS=addr [WORKER_NAME=name] [ACCOUNT_ID=id]); nonzero if failed or unverified"
4
+ task check_route: :environment do
5
+ require "cloudflare/email/check_route_task"
6
+ exit Cloudflare::Email::CheckRouteTask.call(
7
+ address: ENV["ADDRESS"], worker_name: ENV["WORKER_NAME"] || ENV["WORKER"], account_id: ENV["ACCOUNT_ID"],
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ # Supply these in the Cloudflare deployment form. Never commit real values.
2
+ RAILS_INGRESS_URL=
3
+ INGRESS_SECRET=
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ .wrangler/
3
+ .dev.vars*
4
+ !.dev.vars.example
5
+ .env*
6
+ !.env.example
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cole
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,84 @@
1
+ # Cloudflare Email for Rails
2
+
3
+ Receive email in Rails with the [cloudflare-email gem](https://github.com/cole-robertson/cloudflare-email).
4
+ This template stores inbound mail in private R2, delivers it through a Queue,
5
+ and retries from storage when Rails is unavailable. It includes scheduled
6
+ recovery and Worker logs. No React app or extra server is needed.
7
+
8
+ [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cole-robertson/cloudflare-email/tree/main/templates/deploy-to-cloudflare)
9
+
10
+ ## 1. Prepare Rails
11
+
12
+ Install `gem "cloudflare-email", "~> 0.3.0"` and follow the
13
+ [Rails setup guide](https://github.com/cole-robertson/cloudflare-email/blob/main/docs/getting-started.md).
14
+ Deploy Rails with Action Mailbox storage and routing configured before connecting
15
+ real email. Generate a secret with `openssl rand -hex 32` and configure it as
16
+ `cloudflare.ingress_secret` in Rails credentials or `CLOUDFLARE_INGRESS_SECRET`.
17
+ Keep the same value for the deployment form.
18
+
19
+ ## 2. Deploy the Worker
20
+
21
+ Click the button and sign in to Cloudflare and GitHub/GitLab as prompted.
22
+ Cloudflare copies this directory into your own repository, provisions the R2
23
+ bucket and Queue, and builds the Worker. Enter:
24
+
25
+ - **RAILS_INGRESS_URL:** your public HTTPS endpoint, normally
26
+ `https://your-app.com/rails/action_mailbox/cloudflare/inbound_emails`.
27
+ - **INGRESS_SECRET:** the exact secret you configured in Rails.
28
+
29
+ Choose distinct Worker, bucket, and Queue names for each app/environment. This
30
+ template has one environment per deployment; deploy separate copies for staging
31
+ and production. R2 and Queues must be available on your account and their usage
32
+ can incur charges. Review Cloudflare's resource and billing prompts.
33
+
34
+ The form uses `npm run deploy`; no `--env` argument is required for this template.
35
+ After deployment, confirm both bindings, the Queue consumer, and the once-per-minute
36
+ cron are present. Keep the bucket private with no lifecycle rule expiring
37
+ `cloudflare-email/pending/` objects. The Worker has no public email-submit HTTP API.
38
+
39
+ ## 3. Connect an email address
40
+
41
+ In Cloudflare, enable Email Routing for the intended receiving domain/subdomain
42
+ and complete its required DNS setup. Add an Email Routing rule for your receiving
43
+ address with **Send to a Worker**, selecting the Worker you just deployed.
44
+ The button does not change your MX records, provision addresses in Rails, or
45
+ replace your existing mailbox provider. Use a receiving subdomain when your apex
46
+ domain's email belongs to Google Workspace or Microsoft 365.
47
+
48
+ If using the gem's managed mailboxes, register the receiving mailbox/address in
49
+ Rails too. Tenant support and domain catch-all receiving remain explicit choices.
50
+ Outbound Email Sending credentials and delivery-event subscriptions are separate
51
+ from this inbound Worker setup.
52
+
53
+ ## 4. Verify and operate
54
+
55
+ Send to the configured address and verify that Rails retains and routes it.
56
+ In staging, stop Rails, send another message, confirm it remains in R2, restore
57
+ Rails, and confirm one inbox record and removal of the pending object. Verify a
58
+ retry does not duplicate application work. Monitor Worker errors and pending R2
59
+ count/age; Queue depth alone misses mail whose queue retries are exhausted.
60
+
61
+ Read the [durable delivery and recovery guide](https://github.com/cole-robertson/cloudflare-email/blob/main/templates/worker/docs/durable-inbound.md)
62
+ for recovery limits, retention, failure drills, and the explicit direct fallback.
63
+ In this standalone template, configure bindings at the top level and run
64
+ `npm run deploy` without `--env`. Do not remove storage or cron while mail is pending.
65
+ Delivery is at least once, with Rails deduplication; application handlers must
66
+ also tolerate retries. Configure operational alerts in your monitoring service.
67
+
68
+ ## Updates and local development
69
+
70
+ Your copied repository belongs to you; gem updates do not update its Worker
71
+ automatically. Review upstream release notes, deploy Rails first, then port Worker
72
+ changes into your copy while preserving your resource names, secrets, and pending
73
+ storage. Keep production resources separate from test resources.
74
+
75
+ Use Node 22.12+ (or a supported newer version): `npm ci`, `npm test`, and
76
+ `npm run check`. For local development, copy `.dev.vars.example` to `.dev.vars`,
77
+ set local values, and run `npm run dev`. Never commit secrets.
78
+
79
+ Upstream maintainers: `templates/worker` is the canonical runtime. Run
80
+ `node script/sync_deploy_template.mjs` from the gem repository after modifying its
81
+ runtime, tests, dependencies, or production bindings. CI rejects drift and tests
82
+ this directory after copying it outside the repository, matching Cloudflare's
83
+ subdirectory deployment model. Edit this README directly; generated files are
84
+ marked by the sync script's file list.