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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +149 -1
- data/README.md +287 -385
- data/SECURITY.md +51 -0
- data/app/controllers/cloudflare/email/ingress_controller.rb +33 -21
- data/app/controllers/cloudflare/email/management/mailboxes_controller.rb +173 -0
- data/app/controllers/cloudflare/email/management/styles_controller.rb +13 -0
- data/app/views/cloudflare/email/management/mailboxes/index.html.erb +65 -0
- data/app/views/cloudflare/email/management/mailboxes/message.html.erb +34 -0
- data/app/views/cloudflare/email/management/mailboxes/show.html.erb +86 -0
- data/app/views/layouts/cloudflare/email/management.html.erb +29 -0
- data/docs/activerecord-tenanted.md +181 -0
- data/docs/architecture.md +105 -0
- data/docs/custom-ingress.md +207 -0
- data/docs/delivery-events.md +186 -0
- data/docs/features.md +138 -0
- data/docs/getting-started.md +308 -0
- data/docs/mailboxes.md +363 -0
- data/docs/management-engine.md +190 -0
- data/docs/outbox.md +175 -0
- data/docs/routing-deliveries.md +94 -0
- data/docs/routing-diagnostics.md +68 -0
- data/docs/tenant-mailboxes-plan.md +149 -0
- data/docs/thread-correlation.md +27 -0
- data/docs/troubleshooting.md +104 -0
- data/docs/upgrading-0.2.md +101 -0
- data/docs/upgrading-0.3.md +55 -0
- data/docs/verification/2026-09-10-followup.md +49 -0
- data/docs/verification/2026-09-10-install-upgrade.md +86 -0
- data/docs/verification/2026-09-10-live.md +102 -0
- data/docs/verification/2026-09-10.md +175 -0
- data/docs/verification/2026-09-11-custom-ingress.md +49 -0
- data/docs/verification/2026-09-11-extraction.md +32 -0
- data/docs/verification/2026-09-11-outbound-ledger.md +60 -0
- data/docs/verification/2026-09-11-security.md +76 -0
- data/docs/verification/2026-09-11-tenant-mailboxes.md +75 -0
- data/docs/verification/2026-09-12-destination-context.md +46 -0
- data/docs/verification/2026-09-13-durable-inbound-and-routing.md +71 -0
- data/examples/plain_ruby.rb +24 -0
- data/lib/cloudflare/email/active_record/base.rb +60 -0
- data/lib/cloudflare/email/active_record/delivery_events.rb +72 -0
- data/lib/cloudflare/email/active_record/event_inbox.rb +77 -0
- data/lib/cloudflare/email/active_record/event_receipt.rb +17 -0
- data/lib/cloudflare/email/active_record/mail_snapshot.rb +24 -0
- data/lib/cloudflare/email/active_record/outbound_delivery.rb +22 -0
- data/lib/cloudflare/email/active_record/outbound_recipient.rb +13 -0
- data/lib/cloudflare/email/active_record/outbound_reconciliation.rb +15 -0
- data/lib/cloudflare/email/active_record/outbox.rb +219 -0
- data/lib/cloudflare/email/active_record/outbox_notifications.rb +34 -0
- data/lib/cloudflare/email/active_record/routing_deliveries.rb +128 -0
- data/lib/cloudflare/email/active_record.rb +5 -0
- data/lib/cloudflare/email/check_route_task.rb +21 -0
- data/lib/cloudflare/email/client.rb +93 -18
- data/lib/cloudflare/email/consume_events_task.rb +22 -0
- data/lib/cloudflare/email/delivery_event.rb +70 -0
- data/lib/cloudflare/email/delivery_method.rb +5 -0
- data/lib/cloudflare/email/deploy_worker_task.rb +6 -3
- data/lib/cloudflare/email/dev_ingress_guard.rb +27 -0
- data/lib/cloudflare/email/dev_tunnel.rb +31 -5
- data/lib/cloudflare/email/doctor.rb +16 -38
- data/lib/cloudflare/email/endpoint.rb +24 -0
- data/lib/cloudflare/email/engine.rb +33 -0
- data/lib/cloudflare/email/envelope.rb +58 -0
- data/lib/cloudflare/email/event_consumer.rb +95 -0
- data/lib/cloudflare/email/ingress.rb +115 -0
- data/lib/cloudflare/email/mailboxes/configuration.rb +58 -0
- data/lib/cloudflare/email/mailboxes/events.rb +112 -0
- data/lib/cloudflare/email/mailboxes/inbound_retention.rb +14 -0
- data/lib/cloudflare/email/mailboxes/jobs.rb +34 -0
- data/lib/cloudflare/email/mailboxes/models.rb +165 -0
- data/lib/cloudflare/email/mailboxes/provider_correlation.rb +11 -0
- data/lib/cloudflare/email/mailboxes/service.rb +372 -0
- data/lib/cloudflare/email/mailboxes/shared_event_receipt.rb +14 -0
- data/lib/cloudflare/email/mailboxes.rb +11 -0
- data/lib/cloudflare/email/management/adapter.rb +31 -0
- data/lib/cloudflare/email/management/configuration.rb +17 -0
- data/lib/cloudflare/email/management/engine.rb +15 -0
- data/lib/cloudflare/email/management/management.css +68 -0
- data/lib/cloudflare/email/management/routes.rb +14 -0
- data/lib/cloudflare/email/management.rb +7 -0
- data/lib/cloudflare/email/message_id.rb +12 -0
- data/lib/cloudflare/email/provider_metadata.rb +77 -0
- data/lib/cloudflare/email/provision_catchall_task.rb +4 -4
- data/lib/cloudflare/email/provision_route_task.rb +2 -1
- data/lib/cloudflare/email/replay_events_job.rb +18 -0
- data/lib/cloudflare/email/response.rb +21 -3
- data/lib/cloudflare/email/routing_analytics.rb +196 -0
- data/lib/cloudflare/email/routing_diagnostics.rb +206 -0
- data/lib/cloudflare/email/routing_provisioner.rb +71 -45
- data/lib/cloudflare/email/send_job.rb +41 -0
- data/lib/cloudflare/email/send_test.rb +5 -29
- data/lib/cloudflare/email/signing.rb +2 -2
- data/lib/cloudflare/email/tenancy.rb +78 -0
- data/lib/cloudflare/email/tenant_job_context.rb +90 -0
- data/lib/cloudflare/email/verification.rb +38 -8
- data/lib/cloudflare/email/version.rb +1 -1
- data/lib/cloudflare/email/worker_deployer.rb +41 -8
- data/lib/cloudflare-email.rb +4 -1
- data/lib/generators/cloudflare/email/install_generator.rb +53 -37
- 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/mailboxes_generator.rb +55 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_mailboxes.rb +58 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_receiving_domains.rb +16 -0
- data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_shared_events.rb +35 -0
- data/lib/generators/cloudflare/email/outbox/outbox_generator.rb +28 -0
- data/lib/generators/cloudflare/email/outbox/templates/create_cloudflare_email_outbox.rb +48 -0
- 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/generators/cloudflare/email/templates/initializer.rb +3 -3
- data/lib/generators/cloudflare/email/templates/main_mailbox.rb +2 -7
- data/lib/generators/cloudflare/email/tracking/templates/create_cloudflare_email_event_receipts.rb +22 -0
- data/lib/generators/cloudflare/email/tracking/tracking_generator.rb +26 -0
- data/lib/tasks/cloudflare_email.rake +42 -4
- 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 +84 -0
- data/templates/deploy-to-cloudflare/package-lock.json +2627 -0
- data/templates/deploy-to-cloudflare/package.json +36 -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/vitest.config.ts +8 -0
- data/templates/deploy-to-cloudflare/wrangler.toml +22 -0
- data/templates/worker/README.md +231 -16
- data/templates/worker/docs/durable-inbound.md +149 -0
- data/templates/worker/package-lock.json +2627 -0
- data/templates/worker/package.json +8 -5
- data/templates/worker/scripts/wrangler.mjs +16 -0
- data/templates/worker/src/index.js +397 -25
- data/templates/worker/test/durable.test.ts +193 -0
- data/templates/worker/test/index.test.ts +183 -13
- data/templates/worker/test/relay.test.ts +226 -0
- data/templates/worker/wrangler.toml +63 -4
- metadata +154 -7
- 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
|
|
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
|
|
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
|
|
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,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
|
+
[](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.
|