Class: Cloudflare::Email::RoutingProvisioner
- Inherits:
-
Object
- Object
- Cloudflare::Email::RoutingProvisioner
- Defined in:
- lib/cloudflare/email/routing_provisioner.rb
Overview
Provision Cloudflare Email Routing rules via API — no dashboard clicks.
Looks up the zone ID for a domain, enables Email Routing on the zone (publishing the MX + SPF records Cloudflare needs), and creates/updates a rule sending mail for a specific address to a given Worker.
Required API token scopes:
Zone → Zone → Read (to look up zone by name)
Zone → Email Routing → Edit (to enable routing and add rules)
Usage:
provisioner = Cloudflare::Email::RoutingProvisioner.new(
api_token: ENV["CLOUDFLARE_API_TOKEN"],
)
provisioner.provision(
address: "cole@in.example.com",
worker_name: "cloudflare-email-ingress-production",
)
Constant Summary collapse
- API_BASE =
"https://api.cloudflare.com/client/v4".freeze
Instance Method Summary collapse
- #enable_routing_if_needed(zone_id) ⇒ Object
- #find_rule_for(zone_id:, address:) ⇒ Object
- #find_zone_id_for(domain) ⇒ Object
-
#initialize(api_token:, api_base: API_BASE) ⇒ RoutingProvisioner
constructor
A new instance of RoutingProvisioner.
- #list_rules(zone_id) ⇒ Object
-
#provision(address:, worker_name:) ⇒ Object
High-level: given an address + Worker name, do everything needed to make that address route to that Worker.
-
#provision_catch_all(zone_id:, worker_name:) ⇒ Object
Point the zone’s catch-all rule at our Worker.
- #provision_catch_all_for_domain(domain:, worker_name:) ⇒ Object
- #upsert_route(zone_id:, address:, worker_name:) ⇒ Object
Constructor Details
#initialize(api_token:, api_base: API_BASE) ⇒ RoutingProvisioner
Returns a new instance of RoutingProvisioner.
28 29 30 31 32 |
# File 'lib/cloudflare/email/routing_provisioner.rb', line 28 def initialize(api_token:, api_base: API_BASE) raise ArgumentError, "api_token is required" if api_token.to_s.empty? @api_token = api_token @api_base = api_base end |
Instance Method Details
#enable_routing_if_needed(zone_id) ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/cloudflare/email/routing_provisioner.rb', line 61 def enable_routing_if_needed(zone_id) # This endpoint requires the "Email Routing Settings" permission group, # which most scoped tokens don't carry. If we can read the setting, # enable when off. If we can't (403), assume the user enabled routing # via the dashboard when they added the subdomain — the subsequent # rule create will fail with a clear error if not. current = raw_api_request(:get, "/zones/#{zone_id}/email/routing") status = current.code.to_i case status when 200 body = parse(current.body) enabled = body.dig("result", "enabled") api_request(:post, "/zones/#{zone_id}/email/routing/enable") unless enabled when 403, 404 # Either the token can't read settings or routing isn't set up. # Try to enable optimistically; ignore failure (rule create will # surface a precise error if routing is actually off). attempt = raw_api_request(:post, "/zones/#{zone_id}/email/routing/enable") # Don't fail here even if this also 403s — move on to rule creation. else handle!(current, "GET /zones/#{zone_id}/email/routing") end end |
#find_rule_for(zone_id:, address:) ⇒ Object
112 113 114 115 116 117 118 119 120 |
# File 'lib/cloudflare/email/routing_provisioner.rb', line 112 def find_rule_for(zone_id:, address:) result = api_request(:get, "/zones/#{zone_id}/email/routing/rules?per_page=50") rules = Array(result["result"]) rules.find do |r| matchers = Array(r["matchers"]) matchers.any? { |m| m["field"] == "to" && m["value"] == address } end end |
#find_zone_id_for(domain) ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/cloudflare/email/routing_provisioner.rb', line 46 def find_zone_id_for(domain) # Try the exact domain, then walk up parent domains until we find a # Cloudflare zone. Supports subdomains like "in.example.com" routing # to the "example.com" zone. candidates = (domain) candidates.each do |candidate| result = api_request(:get, "/zones?name=#{URI.encode_www_form_component(candidate)}") zones = Array(result["result"]) return zones.first["id"] if zones.any? end nil end |
#list_rules(zone_id) ⇒ Object
122 123 124 125 |
# File 'lib/cloudflare/email/routing_provisioner.rb', line 122 def list_rules(zone_id) result = api_request(:get, "/zones/#{zone_id}/email/routing/rules?per_page=50") Array(result["result"]) end |
#provision(address:, worker_name:) ⇒ Object
High-level: given an address + Worker name, do everything needed to make that address route to that Worker. Idempotent — running twice is safe and will update the existing rule rather than duplicate it.
37 38 39 40 41 42 43 44 |
# File 'lib/cloudflare/email/routing_provisioner.rb', line 37 def provision(address:, worker_name:) domain = extract_domain(address) zone_id = find_zone_id_for(domain) raise Error.new("No Cloudflare zone found for #{domain} — add the domain to your account first") unless zone_id enable_routing_if_needed(zone_id) upsert_route(zone_id: zone_id, address: address, worker_name: worker_name) end |
#provision_catch_all(zone_id:, worker_name:) ⇒ Object
Point the zone’s catch-all rule at our Worker. Catch-all matches any address on the zone that isn’t covered by a more specific rule. Useful for bounce handling, dev subdomains, alias routing.
130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/cloudflare/email/routing_provisioner.rb', line 130 def provision_catch_all(zone_id:, worker_name:) api_request( :put, "/zones/#{zone_id}/email/routing/rules/catch_all", body: { name: "cloudflare-email gem — catch-all", enabled: true, matchers: [{ type: "all" }], actions: [{ type: "worker", value: [worker_name] }], }, ) end |
#provision_catch_all_for_domain(domain:, worker_name:) ⇒ Object
143 144 145 146 147 148 149 |
# File 'lib/cloudflare/email/routing_provisioner.rb', line 143 def provision_catch_all_for_domain(domain:, worker_name:) zone_id = find_zone_id_for(domain) raise Error.new("No Cloudflare zone found for #{domain}") unless zone_id enable_routing_if_needed(zone_id) provision_catch_all(zone_id: zone_id, worker_name: worker_name) end |
#upsert_route(zone_id:, address:, worker_name:) ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/cloudflare/email/routing_provisioner.rb', line 86 def upsert_route(zone_id:, address:, worker_name:) existing = find_rule_for(zone_id: zone_id, address: address) rule = { name: "cloudflare-email gem — #{address}", enabled: true, priority: 0, matchers: [{ field: "to", type: "literal", value: address }], actions: [{ type: "worker", value: [worker_name] }], } if existing api_request( :put, "/zones/#{zone_id}/email/routing/rules/#{existing['id']}", body: rule, ) else api_request( :post, "/zones/#{zone_id}/email/routing/rules", body: rule, ) end end |