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
data/lib/cloudflare-email.rb
CHANGED
|
@@ -2,8 +2,11 @@ require "cloudflare/email/version"
|
|
|
2
2
|
require "cloudflare/email/error"
|
|
3
3
|
require "cloudflare/email/response"
|
|
4
4
|
require "cloudflare/email/signing"
|
|
5
|
+
require "cloudflare/email/envelope"
|
|
5
6
|
require "cloudflare/email/credentials"
|
|
6
7
|
require "cloudflare/email/client"
|
|
7
|
-
require "cloudflare/email/
|
|
8
|
+
require "cloudflare/email/message_id"
|
|
9
|
+
require "cloudflare/email/delivery_event"
|
|
10
|
+
require "cloudflare/email/event_consumer"
|
|
8
11
|
|
|
9
12
|
require "cloudflare/email/engine" if defined?(::Rails::Engine)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "rails/generators"
|
|
1
2
|
require "rails/generators/base"
|
|
2
3
|
require "securerandom"
|
|
3
4
|
|
|
@@ -16,7 +17,7 @@ module Cloudflare
|
|
|
16
17
|
desc: "Directory to copy the Cloudflare Worker template into"
|
|
17
18
|
|
|
18
19
|
class_option :all_envs, type: :boolean, default: false,
|
|
19
|
-
desc: "Also configure action_mailbox.ingress in
|
|
20
|
+
desc: "Also configure action_mailbox.ingress in test.rb (development and production are configured by default)"
|
|
20
21
|
|
|
21
22
|
class_option :deploy_worker, type: :boolean, default: nil,
|
|
22
23
|
desc: "Deploy the Worker via wrangler after setup (nil = interactive prompt)"
|
|
@@ -74,8 +75,8 @@ module Cloudflare
|
|
|
74
75
|
def configure_action_mailbox_ingress
|
|
75
76
|
return unless options[:inbound]
|
|
76
77
|
|
|
77
|
-
envs = ["production"]
|
|
78
|
-
envs
|
|
78
|
+
envs = ["development", "production"]
|
|
79
|
+
envs << "test" if options[:all_envs]
|
|
79
80
|
|
|
80
81
|
envs.each do |env|
|
|
81
82
|
file = "config/environments/#{env}.rb"
|
|
@@ -92,7 +93,7 @@ module Cloudflare
|
|
|
92
93
|
return unless options[:inbound]
|
|
93
94
|
|
|
94
95
|
worker_src = File.expand_path("../../../../templates/worker", __dir__)
|
|
95
|
-
directory worker_src, options[:worker_dir]
|
|
96
|
+
directory worker_src, options[:worker_dir], exclude_pattern: %r{/(node_modules|\.wrangler|\.dev\.vars(?:\.[^/]*)?|\.env(?:\.[^/]*)?)(/|$)}
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
def maybe_deploy_worker
|
|
@@ -101,37 +102,45 @@ module Cloudflare
|
|
|
101
102
|
should_deploy = options[:deploy_worker]
|
|
102
103
|
if should_deploy.nil?
|
|
103
104
|
say ""
|
|
104
|
-
say "
|
|
105
|
-
say "
|
|
106
|
-
say " Run `bin/rails cloudflare:email:deploy_worker URL=https://yourapp.com#{ingress_path}`"
|
|
105
|
+
say " First provision durable storage, Queue and cron using the generated Worker docs/durable-inbound.md."
|
|
106
|
+
say " After the initial Wrangler deploy, Ruby uploads preserve those infrastructure bindings."
|
|
107
|
+
say " Run `RAILS_ENV=production bin/rails cloudflare:email:deploy_worker SCRIPT=#{worker_script_argument} URL=https://yourapp.com#{ingress_path}`"
|
|
107
108
|
say " after `bin/rails credentials:edit`."
|
|
108
109
|
say ""
|
|
109
110
|
say " Alternatively, deploy now via wrangler if it's installed locally." if wrangler_installed?
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
if should_deploy
|
|
113
|
+
if should_deploy
|
|
114
|
+
raise Thor::Error, "Install wrangler or use the cloudflare:email:deploy_worker Rails task" unless wrangler_installed?
|
|
113
115
|
wrangler_deploy
|
|
114
116
|
end
|
|
115
117
|
end
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
no_tasks do
|
|
120
|
+
def wrangler_deploy
|
|
121
|
+
@ingress_secret = SecureRandom.hex(32)
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
inside options[:worker_dir] do
|
|
124
|
+
say "Create the R2 bucket and Queue in docs/durable-inbound.md before deploying. Customize wrangler.toml names if needed."
|
|
125
|
+
run "npm ci", abort_on_failure: true
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
ingress_url = ask("Rails ingress URL? (e.g. https://yourapp.com#{ingress_path})")
|
|
128
|
+
if ingress_url.to_s.strip.empty?
|
|
129
|
+
say " Skipping Worker deploy — no URL supplied. Re-run `npm run deploy -- --env production` manually when ready.", :yellow
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
require "cloudflare/email/endpoint"
|
|
134
|
+
Cloudflare::Email::Endpoint.parse(ingress_url.strip)
|
|
135
|
+
|
|
136
|
+
run "npm run deploy -- --env production", abort_on_failure: true
|
|
137
|
+
run_with_stdin("npx --no-install wrangler secret put RAILS_INGRESS_URL --env production", ingress_url.strip)
|
|
138
|
+
run_with_stdin("npx --no-install wrangler secret put INGRESS_SECRET --env production", @ingress_secret)
|
|
127
139
|
end
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
run_with_stdin("wrangler secret put INGRESS_SECRET", @ingress_secret)
|
|
131
|
-
run "wrangler deploy", abort_on_failure: false
|
|
141
|
+
@worker_deployed = true
|
|
132
142
|
end
|
|
133
|
-
|
|
134
|
-
@worker_deployed = true
|
|
143
|
+
private :wrangler_deploy
|
|
135
144
|
end
|
|
136
145
|
|
|
137
146
|
def print_post_install
|
|
@@ -155,41 +164,43 @@ module Cloudflare
|
|
|
155
164
|
say " bin/rails cloudflare:email:doctor"
|
|
156
165
|
say ""
|
|
157
166
|
say " 3. Send a test email:"
|
|
158
|
-
say " TO=you@example.com bin/rails cloudflare:email:send_test"
|
|
167
|
+
say " FROM=hello@your-verified-domain.com TO=you@example.com bin/rails cloudflare:email:send_test"
|
|
159
168
|
say ""
|
|
160
169
|
|
|
161
170
|
if options[:inbound] && !@worker_deployed
|
|
162
171
|
say " 4. Deploy the Worker (pick one):"
|
|
163
|
-
say " #
|
|
164
|
-
say " bin/rails cloudflare:email:deploy_worker URL=https://yourapp.com#{ingress_path}"
|
|
172
|
+
say " # First follow Worker docs/durable-inbound.md to provision R2, Queue and cron."
|
|
173
|
+
say " RAILS_ENV=production bin/rails cloudflare:email:deploy_worker SCRIPT=#{worker_script_argument} URL=https://yourapp.com#{ingress_path}"
|
|
165
174
|
say ""
|
|
166
|
-
say " #
|
|
175
|
+
say " # Initial infrastructure deploy (after provisioning resources):"
|
|
167
176
|
say " cd #{options[:worker_dir]}"
|
|
168
|
-
say " npm
|
|
169
|
-
say "
|
|
170
|
-
say " wrangler secret put
|
|
171
|
-
say " wrangler
|
|
177
|
+
say " npm ci"
|
|
178
|
+
say " npm run deploy -- --env production"
|
|
179
|
+
say " npx --no-install wrangler secret put INGRESS_SECRET --env production # paste #{@ingress_secret[0, 8]}..."
|
|
180
|
+
say " npx --no-install wrangler secret put RAILS_INGRESS_URL --env production # https://yourapp.com#{ingress_path}"
|
|
172
181
|
say ""
|
|
173
182
|
end
|
|
174
183
|
|
|
175
184
|
if options[:inbound]
|
|
176
185
|
say " 5. For local dev (tunnels cloudflared to your Worker):"
|
|
177
|
-
say " bin/rails cloudflare:email:
|
|
186
|
+
say " INBOUND_DELIVERY_MODE=direct RAILS_ENV=development bin/rails cloudflare:email:deploy_worker SCRIPT=#{worker_script_argument}"
|
|
187
|
+
say " RAILS_ENV=development bin/rails cloudflare:email:dev"
|
|
188
|
+
say " Route a separate test address to cloudflare-email-ingress-development."
|
|
178
189
|
say ""
|
|
179
190
|
say " 6. In the Cloudflare dashboard:"
|
|
180
191
|
say " Email Routing -> Routes -> Send to a Worker -> #{worker_name}"
|
|
181
192
|
say ""
|
|
182
193
|
say " Dashboard deep-links:"
|
|
183
194
|
say " API tokens: https://dash.cloudflare.com/profile/api-tokens"
|
|
184
|
-
say " Sending domains: https://dash.cloudflare.com/?to=/:account/email/sending"
|
|
185
|
-
say " Email routing: https://dash.cloudflare.com/?to=/:account/email/routing"
|
|
195
|
+
say " Sending domains: https://dash.cloudflare.com/?to=/:account/email-service/sending"
|
|
196
|
+
say " Email routing: https://dash.cloudflare.com/?to=/:account/email-service/routing"
|
|
186
197
|
say ""
|
|
187
198
|
say " Rotation: to rotate the ingress secret, update cloudflare.ingress_secret"
|
|
188
|
-
say " in Rails credentials AND re-run `wrangler secret put INGRESS_SECRET` in"
|
|
199
|
+
say " in Rails credentials AND re-run `wrangler secret put INGRESS_SECRET --env production` in"
|
|
189
200
|
say " #{options[:worker_dir]}/ with the new value, then redeploy the Worker."
|
|
190
201
|
say ""
|
|
191
|
-
say "
|
|
192
|
-
say " Re-run with --all-envs to also configure
|
|
202
|
+
say " Development and production are wired to :cloudflare ingress."
|
|
203
|
+
say " Re-run with --all-envs to also configure test.rb."
|
|
193
204
|
end
|
|
194
205
|
say ""
|
|
195
206
|
end
|
|
@@ -211,14 +222,19 @@ module Cloudflare
|
|
|
211
222
|
end
|
|
212
223
|
|
|
213
224
|
def worker_name
|
|
214
|
-
"cloudflare-email-ingress"
|
|
225
|
+
"cloudflare-email-ingress-production"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def worker_script_argument
|
|
229
|
+
require "shellwords"
|
|
230
|
+
Shellwords.escape(File.join(options[:worker_dir], "src/index.js"))
|
|
215
231
|
end
|
|
216
232
|
|
|
217
233
|
def run_with_stdin(cmd, input)
|
|
218
234
|
require "open3"
|
|
219
235
|
out, err, status = Open3.capture3(cmd, stdin_data: input + "\n")
|
|
220
236
|
unless status.success?
|
|
221
|
-
|
|
237
|
+
raise Thor::Error, "#{cmd} failed: #{err.empty? ? out : err}"
|
|
222
238
|
end
|
|
223
239
|
end
|
|
224
240
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module Cloudflare
|
|
5
|
+
module Email
|
|
6
|
+
module Generators
|
|
7
|
+
class CatchAllGenerator < ::Rails::Generators::Base
|
|
8
|
+
include ::ActiveRecord::Generators::Migration
|
|
9
|
+
namespace "cloudflare:email:mailboxes:catch_all"
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
class_option :tenant_migrations_path, type: :string, default: "db/migrate",
|
|
12
|
+
desc: "Migration directory applied to every tenant database"
|
|
13
|
+
|
|
14
|
+
def copy_migration
|
|
15
|
+
migration_template "add_cloudflare_email_catch_all.rb",
|
|
16
|
+
File.join(options[:tenant_migrations_path], "add_cloudflare_email_catch_all.rb")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/generators/cloudflare/email/mailboxes/catch_all/templates/add_cloudflare_email_catch_all.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class AddCloudflareEmailCatchAll < ActiveRecord::Migration[7.1]
|
|
2
|
+
def up
|
|
3
|
+
unless column_exists?(:cloudflare_email_addresses, :catch_all)
|
|
4
|
+
add_column :cloudflare_email_addresses, :catch_all, :boolean, null: false, default: false
|
|
5
|
+
end
|
|
6
|
+
unless column_exists?(:cloudflare_email_addresses, :catch_all_evidence)
|
|
7
|
+
add_column :cloudflare_email_addresses, :catch_all_evidence, :text
|
|
8
|
+
end
|
|
9
|
+
unless index_exists?(:cloudflare_email_addresses, :receiving_domain_id, name: "idx_cf_email_domain_catch_all")
|
|
10
|
+
add_index :cloudflare_email_addresses, :receiving_domain_id, unique: true,
|
|
11
|
+
where: "catch_all = TRUE AND state = 'active'", name: "idx_cf_email_domain_catch_all"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def down
|
|
16
|
+
raise ActiveRecord::IrreversibleMigration, "Disable catch-all receiving before an explicit forward schema change"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
require "generators/cloudflare/email/outbox/outbox_generator"
|
|
4
|
+
require "generators/cloudflare/email/tracking/tracking_generator"
|
|
5
|
+
|
|
6
|
+
module Cloudflare
|
|
7
|
+
module Email
|
|
8
|
+
module Generators
|
|
9
|
+
class MailboxesGenerator < ::Rails::Generators::Base
|
|
10
|
+
include ::ActiveRecord::Generators::Migration
|
|
11
|
+
namespace "cloudflare:email:mailboxes"
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
class_option :tenant_migrations_path, type: :string, default: "db/migrate",
|
|
14
|
+
desc: "Migration directory applied to every tenant database"
|
|
15
|
+
class_option :directory_migrations_path, type: :string, default: "db/migrate",
|
|
16
|
+
desc: "Migration directory applied only to the shared directory database"
|
|
17
|
+
|
|
18
|
+
def copy_migrations
|
|
19
|
+
invoke "cloudflare:email:outbox", [], migrations_path: options[:tenant_migrations_path]
|
|
20
|
+
invoke "cloudflare:email:tracking", [], migrations_path: options[:tenant_migrations_path]
|
|
21
|
+
migration_template "create_cloudflare_email_receiving_domains.rb",
|
|
22
|
+
File.join(options[:directory_migrations_path], "create_cloudflare_email_receiving_domains.rb")
|
|
23
|
+
migration_template "create_cloudflare_email_shared_events.rb",
|
|
24
|
+
File.join(options[:directory_migrations_path], "create_cloudflare_email_shared_events.rb")
|
|
25
|
+
migration_template "create_cloudflare_email_mailboxes.rb",
|
|
26
|
+
File.join(options[:tenant_migrations_path], "create_cloudflare_email_mailboxes.rb")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_initializer
|
|
30
|
+
create_file "config/initializers/00_cloudflare_email_tenancy.rb", <<~RUBY
|
|
31
|
+
# Single database: no configuration is necessary.
|
|
32
|
+
# Separate tenant databases: configure BEFORE the optional models load.
|
|
33
|
+
# Replace these application-specific adapter names with your own:
|
|
34
|
+
# require "cloudflare/email/tenancy"
|
|
35
|
+
# Cloudflare::Email::Tenancy.configure(
|
|
36
|
+
# base_class: TenantRecord,
|
|
37
|
+
# switch: ->(key, &block) { TenantRecord.with_tenant(key, &block) },
|
|
38
|
+
# current: -> { TenantRecord.current_tenant }
|
|
39
|
+
# )
|
|
40
|
+
# require "cloudflare/email/mailboxes/configuration"
|
|
41
|
+
# Cloudflare::Email::Mailboxes.configure(directory_base: SharedRecord)
|
|
42
|
+
# ActionMailbox and ActiveStorage must use this same tenant connection.
|
|
43
|
+
# See docs/mailboxes.md for the complete application setup.
|
|
44
|
+
RUBY
|
|
45
|
+
create_file "config/initializers/cloudflare_email_mailboxes.rb", <<~RUBY
|
|
46
|
+
# Optional mailbox persistence. Run the generated migrations first.
|
|
47
|
+
# For database tenancy, configure Tenancy and the shared directory
|
|
48
|
+
# base before requiring cloudflare/email/mailboxes. See docs/mailboxes.md.
|
|
49
|
+
require "cloudflare/email/mailboxes"
|
|
50
|
+
RUBY
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
class CreateCloudflareEmailMailboxes < ActiveRecord::Migration[7.1]
|
|
2
|
+
def up
|
|
3
|
+
create_table :cloudflare_email_mailboxes do |t|
|
|
4
|
+
t.string :tenant_key, null: false
|
|
5
|
+
t.string :name, null: false
|
|
6
|
+
t.string :owner_ref
|
|
7
|
+
t.string :state, null: false, default: "active"
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
add_index :cloudflare_email_mailboxes, [:tenant_key, :owner_ref], name: "idx_cf_email_mailbox_owner"
|
|
11
|
+
|
|
12
|
+
create_table :cloudflare_email_addresses do |t|
|
|
13
|
+
t.string :tenant_key, null: false
|
|
14
|
+
t.references :mailbox, null: false, index: false, foreign_key: { to_table: :cloudflare_email_mailboxes }
|
|
15
|
+
# The domain directory can live in another database: no cross-database FK.
|
|
16
|
+
t.bigint :receiving_domain_id, null: false
|
|
17
|
+
t.string :local_part, null: false
|
|
18
|
+
t.string :domain, null: false
|
|
19
|
+
t.string :address, null: false
|
|
20
|
+
t.string :state, null: false, default: "pending"
|
|
21
|
+
t.text :provisioning_evidence
|
|
22
|
+
t.boolean :catch_all, null: false, default: false
|
|
23
|
+
t.text :catch_all_evidence
|
|
24
|
+
t.timestamps
|
|
25
|
+
end
|
|
26
|
+
add_index :cloudflare_email_addresses, :address, unique: true, name: "idx_cf_email_mailbox_address"
|
|
27
|
+
add_index :cloudflare_email_addresses, :mailbox_id, name: "idx_cf_email_address_mailbox"
|
|
28
|
+
add_index :cloudflare_email_addresses, :receiving_domain_id, unique: true,
|
|
29
|
+
where: "catch_all = TRUE AND state = 'active'", name: "idx_cf_email_domain_catch_all"
|
|
30
|
+
|
|
31
|
+
create_table :cloudflare_email_mailbox_messages do |t|
|
|
32
|
+
t.string :tenant_key, null: false
|
|
33
|
+
t.references :mailbox, null: false, index: false, foreign_key: { to_table: :cloudflare_email_mailboxes }
|
|
34
|
+
# No database FK to the optional Rails table; the ingress service requires
|
|
35
|
+
# the same connection and manages membership+raw source atomically.
|
|
36
|
+
t.bigint :inbound_email_id, null: false
|
|
37
|
+
t.string :recipient, null: false
|
|
38
|
+
t.datetime :read_at
|
|
39
|
+
t.datetime :archived_at
|
|
40
|
+
t.timestamps
|
|
41
|
+
end
|
|
42
|
+
add_index :cloudflare_email_mailbox_messages, [:mailbox_id, :inbound_email_id], unique: true, name: "idx_cf_email_mailbox_inbound"
|
|
43
|
+
add_index :cloudflare_email_mailbox_messages, [:tenant_key, :mailbox_id, :archived_at, :id], name: "idx_cf_email_mailbox_inbox"
|
|
44
|
+
|
|
45
|
+
create_table :cloudflare_email_mailbox_outbound_messages do |t|
|
|
46
|
+
t.string :tenant_key, null: false
|
|
47
|
+
t.references :mailbox, null: false, index: false, foreign_key: { to_table: :cloudflare_email_mailboxes }
|
|
48
|
+
t.references :outbound_delivery, null: false, index: false, foreign_key: { to_table: :cloudflare_email_outbound_deliveries }
|
|
49
|
+
t.timestamps
|
|
50
|
+
end
|
|
51
|
+
add_index :cloudflare_email_mailbox_outbound_messages, :outbound_delivery_id, unique: true, name: "idx_cf_email_mailbox_outbound"
|
|
52
|
+
add_index :cloudflare_email_mailbox_outbound_messages, :mailbox_id, name: "idx_cf_email_outbound_mailbox"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def down
|
|
56
|
+
raise ActiveRecord::IrreversibleMigration, "Preserve mailbox ownership and retained messages; use a forward fix"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateCloudflareEmailReceivingDomains < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :cloudflare_email_receiving_domains do |t|
|
|
4
|
+
t.string :domain, null: false
|
|
5
|
+
t.string :tenant_key, null: false
|
|
6
|
+
t.string :account_id, null: false
|
|
7
|
+
t.string :state, null: false, default: "pending"
|
|
8
|
+
t.boolean :sending_enabled, null: false, default: false
|
|
9
|
+
t.text :provisioning_evidence
|
|
10
|
+
t.datetime :verified_at
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :cloudflare_email_receiving_domains, :domain, unique: true, name: "idx_cf_email_directory_domain"
|
|
14
|
+
add_index :cloudflare_email_receiving_domains, [:tenant_key, :state], name: "idx_cf_email_directory_tenant"
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/generators/cloudflare/email/mailboxes/templates/create_cloudflare_email_shared_events.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class CreateCloudflareEmailSharedEvents < ActiveRecord::Migration[7.2]
|
|
2
|
+
def up
|
|
3
|
+
create_table :cloudflare_email_shared_event_receipts do |t|
|
|
4
|
+
t.string :account_id, null: false
|
|
5
|
+
t.string :event_id, null: false
|
|
6
|
+
t.string :message_id, null: false
|
|
7
|
+
t.string :recipient, null: false
|
|
8
|
+
t.text :payload_json, null: false
|
|
9
|
+
t.string :state, null: false, default: "pending"
|
|
10
|
+
t.datetime :applied_at
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :cloudflare_email_shared_event_receipts, [:account_id, :event_id], unique: true,
|
|
14
|
+
name: "idx_cf_shared_events_identity"
|
|
15
|
+
add_index :cloudflare_email_shared_event_receipts, [:state, :id], name: "idx_cf_shared_events_replay"
|
|
16
|
+
|
|
17
|
+
create_table :cloudflare_email_provider_correlations do |t|
|
|
18
|
+
t.string :account_id, null: false
|
|
19
|
+
t.string :message_id, null: false
|
|
20
|
+
t.string :recipient, null: false
|
|
21
|
+
t.string :tenant_key, null: false
|
|
22
|
+
t.bigint :outbound_delivery_id, null: false
|
|
23
|
+
t.timestamps
|
|
24
|
+
end
|
|
25
|
+
add_index :cloudflare_email_provider_correlations,
|
|
26
|
+
[:account_id, :message_id, :recipient, :tenant_key, :outbound_delivery_id], unique: true,
|
|
27
|
+
name: "idx_cf_provider_correlations_identity"
|
|
28
|
+
add_index :cloudflare_email_provider_correlations, [:account_id, :message_id, :recipient],
|
|
29
|
+
name: "idx_cf_provider_correlations_lookup"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def down
|
|
33
|
+
raise ActiveRecord::IrreversibleMigration, "Preserve tenant routing and event deduplication evidence; use a forward fix"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module Cloudflare
|
|
5
|
+
module Email
|
|
6
|
+
module Generators
|
|
7
|
+
class OutboxGenerator < ::Rails::Generators::Base
|
|
8
|
+
include ::ActiveRecord::Generators::Migration
|
|
9
|
+
namespace "cloudflare:email:outbox"
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
class_option :migrations_path, type: :string, default: "db/migrate"
|
|
12
|
+
|
|
13
|
+
def copy_outbox_migration
|
|
14
|
+
migration_template "create_cloudflare_email_outbox.rb", File.join(options[:migrations_path], "create_cloudflare_email_outbox.rb")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_initializer
|
|
18
|
+
create_file "config/initializers/cloudflare_email_outbox.rb", <<~RUBY
|
|
19
|
+
# Opt in after running db:migrate. Never perform network delivery inside a database transaction.
|
|
20
|
+
require "cloudflare/email/active_record"
|
|
21
|
+
require "cloudflare/email/send_job"
|
|
22
|
+
require "cloudflare/email/replay_events_job"
|
|
23
|
+
RUBY
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class CreateCloudflareEmailOutbox < ActiveRecord::Migration[7.1]
|
|
2
|
+
def up
|
|
3
|
+
create_table :cloudflare_email_outbound_deliveries do |t|
|
|
4
|
+
t.string :account_id, null: false
|
|
5
|
+
t.string :operation_key, null: false
|
|
6
|
+
t.string :from_address, null: false
|
|
7
|
+
t.text :recipients_json, null: false
|
|
8
|
+
t.binary :mime_message, null: false
|
|
9
|
+
t.string :snapshot_digest, null: false
|
|
10
|
+
t.string :state, null: false, default: "prepared"
|
|
11
|
+
t.string :provider_message_id
|
|
12
|
+
t.text :response_json
|
|
13
|
+
t.string :error_class
|
|
14
|
+
t.datetime :request_started_at
|
|
15
|
+
t.datetime :completed_at
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
add_index :cloudflare_email_outbound_deliveries, [:account_id, :operation_key], unique: true, name: "idx_cf_email_outbound_operation"
|
|
19
|
+
add_index :cloudflare_email_outbound_deliveries, [:account_id, :provider_message_id], name: "idx_cf_email_outbound_provider"
|
|
20
|
+
add_index :cloudflare_email_outbound_deliveries, [:state, :id], name: "idx_cf_email_outbound_pending"
|
|
21
|
+
|
|
22
|
+
create_table :cloudflare_email_outbound_recipients do |t|
|
|
23
|
+
t.references :outbound_delivery, null: false, index: false, foreign_key: { to_table: :cloudflare_email_outbound_deliveries }
|
|
24
|
+
t.string :recipient, null: false
|
|
25
|
+
t.string :state, null: false, default: "prepared"
|
|
26
|
+
t.string :acceptance_state, null: false, default: "prepared"
|
|
27
|
+
t.datetime :occurred_at
|
|
28
|
+
t.boolean :terminal, null: false, default: false
|
|
29
|
+
t.timestamps
|
|
30
|
+
end
|
|
31
|
+
add_index :cloudflare_email_outbound_recipients, [:outbound_delivery_id, :recipient], unique: true, name: "idx_cf_email_outbound_recipient"
|
|
32
|
+
|
|
33
|
+
create_table :cloudflare_email_outbound_reconciliations do |t|
|
|
34
|
+
t.references :outbound_delivery, null: false, index: { name: "idx_cf_email_reconciliation_delivery" }, foreign_key: { to_table: :cloudflare_email_outbound_deliveries }
|
|
35
|
+
t.string :actor, null: false
|
|
36
|
+
t.text :reason, null: false
|
|
37
|
+
t.text :evidence, null: false
|
|
38
|
+
t.string :outcome, null: false
|
|
39
|
+
t.string :provider_message_id
|
|
40
|
+
t.text :recipients_json
|
|
41
|
+
t.timestamps
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def down
|
|
46
|
+
raise ActiveRecord::IrreversibleMigration, "Preserve delivery evidence; use a forward fix or a reconciled backup"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module Cloudflare
|
|
5
|
+
module Email
|
|
6
|
+
module Generators
|
|
7
|
+
class RoutingTrackingGenerator < ::Rails::Generators::Base
|
|
8
|
+
include ::ActiveRecord::Generators::Migration
|
|
9
|
+
namespace "cloudflare:email:routing_tracking"
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
class_option :migrations_path, type: :string, default: "db/migrate"
|
|
12
|
+
|
|
13
|
+
def copy_tracking_migration
|
|
14
|
+
migration_template "create_cloudflare_email_routing_delivery_receipts.rb",
|
|
15
|
+
File.join(options[:migrations_path], "create_cloudflare_email_routing_delivery_receipts.rb")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_initializer
|
|
19
|
+
create_file "config/initializers/cloudflare_email_routing_tracking.rb", <<~RUBY
|
|
20
|
+
# Requires the outbox migration and Rails Active Record encryption keys.
|
|
21
|
+
# This loads the API only; your application schedules analytics polling.
|
|
22
|
+
require "cloudflare/email/active_record/routing_deliveries"
|
|
23
|
+
RUBY
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateCloudflareEmailRoutingDeliveryReceipts < ActiveRecord::Migration[7.1]
|
|
2
|
+
def up
|
|
3
|
+
create_table :cloudflare_email_routing_delivery_receipts do |t|
|
|
4
|
+
t.references :outbound_delivery, null: false, index: {name: "idx_cf_routing_delivery"},
|
|
5
|
+
foreign_key: {to_table: :cloudflare_email_outbound_deliveries}
|
|
6
|
+
t.string :account_id, null: false
|
|
7
|
+
t.string :zone_id, null: false
|
|
8
|
+
t.string :event_key, null: false
|
|
9
|
+
t.text :payload_json, null: false
|
|
10
|
+
t.string :state, null: false, default: "pending"
|
|
11
|
+
t.datetime :received_at, null: false
|
|
12
|
+
t.datetime :applied_at
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
add_index :cloudflare_email_routing_delivery_receipts, [:account_id, :event_key], unique: true, name: "idx_cf_routing_identity"
|
|
16
|
+
add_index :cloudflare_email_routing_delivery_receipts, [:account_id, :state, :id], name: "idx_cf_routing_replay"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def down
|
|
20
|
+
raise ActiveRecord::IrreversibleMigration, "Preserve authentic delivery evidence; use a forward fix or a reconciled backup"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
# Inbound: ActionMailbox ingress is mounted at
|
|
9
9
|
# /rails/action_mailbox/cloudflare/inbound_emails
|
|
10
10
|
# Set `config.action_mailbox.ingress = :cloudflare` (the install generator
|
|
11
|
-
# does this for production by default) and configure cloudflare.ingress_secret
|
|
11
|
+
# does this for development and production by default) and configure cloudflare.ingress_secret
|
|
12
12
|
# in your Rails credentials.
|
|
13
13
|
|
|
14
14
|
Rails.application.configure do
|
|
15
15
|
config.action_mailer.delivery_method = :cloudflare
|
|
16
16
|
config.action_mailer.cloudflare_settings = {
|
|
17
|
-
account_id:
|
|
18
|
-
api_token:
|
|
17
|
+
account_id: Cloudflare::Email::Credentials.account_id,
|
|
18
|
+
api_token: Cloudflare::Email::Credentials.api_token,
|
|
19
19
|
}
|
|
20
20
|
end
|
|
@@ -6,13 +6,8 @@
|
|
|
6
6
|
# ActionMailbox API (mail is a Mail::Message, inbound_email is the AR record).
|
|
7
7
|
class MainMailbox < ApplicationMailbox
|
|
8
8
|
def process
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"from=#{mail.from&.first.inspect} " \
|
|
12
|
-
"to=#{Array(mail.to).inspect} " \
|
|
13
|
-
"subject=#{mail.subject.inspect} " \
|
|
14
|
-
"message_id=#{mail.message_id.inspect}"
|
|
15
|
-
)
|
|
9
|
+
# Keep message content and personal addresses out of application logs.
|
|
10
|
+
Rails.logger.info("[cloudflare-email] inbound received: inbound_email_id=#{inbound_email.id}")
|
|
16
11
|
|
|
17
12
|
# Example ways to pull content out of the incoming message:
|
|
18
13
|
#
|
data/lib/generators/cloudflare/email/tracking/templates/create_cloudflare_email_event_receipts.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateCloudflareEmailEventReceipts < ActiveRecord::Migration[7.1]
|
|
2
|
+
def up
|
|
3
|
+
create_table :cloudflare_email_event_receipts do |t|
|
|
4
|
+
t.string :account_id, null: false
|
|
5
|
+
t.string :event_id, null: false
|
|
6
|
+
t.string :message_id, null: false
|
|
7
|
+
t.text :payload_json, null: false
|
|
8
|
+
t.string :state, null: false, default: "pending"
|
|
9
|
+
t.datetime :applied_at
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
add_index :cloudflare_email_event_receipts, [:account_id, :event_id], unique: true,
|
|
13
|
+
name: "idx_cf_email_receipts_account_event"
|
|
14
|
+
add_index :cloudflare_email_event_receipts, [:account_id, :message_id],
|
|
15
|
+
name: "idx_cf_email_receipts_account_message"
|
|
16
|
+
add_index :cloudflare_email_event_receipts, [:state, :id], name: "idx_cf_email_receipts_replay"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def down
|
|
20
|
+
raise ActiveRecord::IrreversibleMigration, "Preserve event deduplication evidence; use a forward fix or a reconciled backup"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module Cloudflare
|
|
5
|
+
module Email
|
|
6
|
+
module Generators
|
|
7
|
+
class TrackingGenerator < ::Rails::Generators::Base
|
|
8
|
+
include ::ActiveRecord::Generators::Migration
|
|
9
|
+
namespace "cloudflare:email:tracking"
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
class_option :migrations_path, type: :string, default: "db/migrate"
|
|
12
|
+
|
|
13
|
+
def copy_tracking_migration
|
|
14
|
+
migration_template "create_cloudflare_email_event_receipts.rb", File.join(options[:migrations_path], "create_cloudflare_email_event_receipts.rb")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_initializer
|
|
18
|
+
create_file "config/initializers/cloudflare_email_tracking.rb", <<~RUBY
|
|
19
|
+
# Opt in to durable delivery-event receipts after running db:migrate.
|
|
20
|
+
require "cloudflare/email/active_record/event_inbox"
|
|
21
|
+
RUBY
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|