cloudflare-email_service 0.2.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 +25 -0
- data/README.md +85 -1
- data/lib/cloudflare/email_service/client.rb +15 -3
- data/lib/cloudflare/email_service/configuration.rb +24 -0
- data/lib/cloudflare/email_service/errors.rb +7 -1
- data/lib/cloudflare/email_service/instrumentation.rb +51 -0
- data/lib/cloudflare/email_service/smtp_client.rb +20 -13
- data/lib/cloudflare/email_service/version.rb +1 -1
- data/lib/cloudflare/email_service.rb +1 -0
- data/lib/generators/cloudflare_email_service/install_generator.rb +46 -0
- data/lib/generators/cloudflare_email_service/templates/cloudflare_email_service.rb +20 -0
- data/templates/cloudflare_email_worker.js +14 -3
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c8b69fe6e9c96aad0aeff171295d1af48d5e68254c5dc7f3bb238c514c86af5
|
|
4
|
+
data.tar.gz: bf78b28870b252900cf5784388ff23523e0d69e27cd4b404e6ae45e3430e0e0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0257f7558d39424c3d8dfbca70519a24fc3ae4d82293d07afd230a0fb7c948f923e16a84ec03504a689f12ca379688c8274a394e4943e7c40b868275698ca9c
|
|
7
|
+
data.tar.gz: 1fca3c06a395a2a73f0301d5ca43e4d55984e7da4df352f21c7b66e65b5e70eb98b102aa5f3ce895344a24997cb883f6af7b49a636c603d01d879aa875fb657a
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-06-21
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- The bundled Email Worker no longer permanently rejects inbound mail on a
|
|
13
|
+
transient ingress failure. A `4xx` from the app is still rejected (permanent
|
|
14
|
+
bounce — a retry won't help), but a `5xx` or network failure now throws, so
|
|
15
|
+
the sending server retries delivery once the app recovers instead of bouncing
|
|
16
|
+
legitimate mail during a deploy or brief outage.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- A Rails install generator: `bin/rails g cloudflare_email_service:install`
|
|
20
|
+
writes `config/initializers/cloudflare_email_service.rb` (inbound lines
|
|
21
|
+
commented out) and prints the remaining setup steps. Generator code lives
|
|
22
|
+
under `lib/generators` and loads only in a Rails generator context, so the
|
|
23
|
+
core gem stays Rails-free.
|
|
24
|
+
- Each send now publishes a `deliver.cloudflare_email_service` instrumentation
|
|
25
|
+
event — through `ActiveSupport::Notifications` when it is loaded, otherwise a
|
|
26
|
+
no-op. The payload carries the transport, recipient counts, and response
|
|
27
|
+
status (never message content); a failed send records the exception. Set
|
|
28
|
+
`config.instrumenter` to plug in a custom one.
|
|
29
|
+
- `RateLimitError#retry_after` exposes the `Retry-After` header from a `429`
|
|
30
|
+
response as an integer number of seconds (nil when absent or sent as an
|
|
31
|
+
HTTP-date), so callers can honor Cloudflare's backoff. The README now
|
|
32
|
+
documents the recommended `deliver_later` + `retry_on` retry pattern.
|
|
33
|
+
|
|
9
34
|
## [0.2.0] - 2026-06-19
|
|
10
35
|
|
|
11
36
|
### Added
|
data/README.md
CHANGED
|
@@ -159,7 +159,14 @@ config.action_mailer.delivery_method = :cloudflare
|
|
|
159
159
|
|
|
160
160
|
Credentials come from `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN` in the
|
|
161
161
|
environment. To set them in code, choose the SMTP transport, or receive inbound
|
|
162
|
-
mail, use a single initializer
|
|
162
|
+
mail, use a single initializer. Generate it with
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
bin/rails g cloudflare_email_service:install
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
which writes the file below (inbound lines commented out) and prints the
|
|
169
|
+
remaining steps — or create it by hand:
|
|
163
170
|
|
|
164
171
|
```ruby
|
|
165
172
|
# config/initializers/cloudflare_email_service.rb
|
|
@@ -224,6 +231,11 @@ above) and `CLOUDFLARE_EMAIL_INGRESS_SECRET` (matching the app):
|
|
|
224
231
|
Visiting the worker's URL returns a `{ ok, configured }` health check — a quick
|
|
225
232
|
way to confirm it's deployed and both vars are set.
|
|
226
233
|
|
|
234
|
+
On a `4xx` (bad signature, wrong media type) the Worker rejects the message
|
|
235
|
+
permanently; on a `5xx` or network failure it fails temporarily, so the sending
|
|
236
|
+
server retries once the app recovers instead of bouncing good mail. Prefer
|
|
237
|
+
zero-downtime deploys, since a long outage can still outlast the retry window.
|
|
238
|
+
|
|
227
239
|
> [!NOTE]
|
|
228
240
|
> The Worker sends `Content-Type: message/rfc822`; the ingress rejects anything
|
|
229
241
|
> else with `415 Unsupported Media Type`.
|
|
@@ -276,6 +288,78 @@ end
|
|
|
276
288
|
|
|
277
289
|
---
|
|
278
290
|
|
|
291
|
+
## Retries
|
|
292
|
+
|
|
293
|
+
This client doesn't retry a failed send — it raises and leaves the retry policy
|
|
294
|
+
to you. Cloudflare already retries _accepted_ mail server-side (soft bounces,
|
|
295
|
+
with exponential backoff); retries here are only about getting the request
|
|
296
|
+
accepted in the first place.
|
|
297
|
+
|
|
298
|
+
In Rails, the idiomatic place is the delivery job: send with `deliver_later` and
|
|
299
|
+
let Active Job retry the transient failures with backoff, while permanent ones
|
|
300
|
+
fail fast. `retry_on` is an Active Job method, so it goes on the delivery job —
|
|
301
|
+
not on the mailer:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# app/jobs/cloudflare_mail_delivery_job.rb
|
|
305
|
+
class CloudflareMailDeliveryJob < ActionMailer::MailDeliveryJob
|
|
306
|
+
retry_on Cloudflare::EmailService::RateLimitError, # 429
|
|
307
|
+
Cloudflare::EmailService::ServerError, # 5xx
|
|
308
|
+
Cloudflare::EmailService::NetworkError, # timeout, connection reset, TLS
|
|
309
|
+
wait: :polynomially_longer, attempts: 5
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# app/mailers/application_mailer.rb
|
|
313
|
+
class ApplicationMailer < ActionMailer::Base
|
|
314
|
+
self.delivery_job = CloudflareMailDeliveryJob
|
|
315
|
+
end
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
On a `429`, Cloudflare may send a `Retry-After` header. When present it's parsed
|
|
319
|
+
to an integer number of seconds and exposed as `RateLimitError#retry_after`, so
|
|
320
|
+
you can honor the backoff precisely:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
rescue Cloudflare::EmailService::RateLimitError => e
|
|
324
|
+
e.retry_after # => 30 (seconds), or nil when not provided
|
|
325
|
+
end
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Instrumentation
|
|
331
|
+
|
|
332
|
+
Every send publishes a `deliver.cloudflare_email_service` event. When
|
|
333
|
+
`ActiveSupport::Notifications` is loaded (e.g. in Rails) it's used
|
|
334
|
+
automatically — subscribe to log, time, or meter your sends:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
ActiveSupport::Notifications.subscribe("deliver.cloudflare_email_service") do |*args|
|
|
338
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
339
|
+
Rails.logger.info(
|
|
340
|
+
"cloudflare email: transport=#{event.payload[:transport]} " \
|
|
341
|
+
"to=#{event.payload[:to]} status=#{event.payload[:status]} " \
|
|
342
|
+
"duration=#{event.duration.round}ms",
|
|
343
|
+
)
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The payload carries `:transport` (`:rest` / `:smtp`), recipient counts (`:to`,
|
|
348
|
+
`:cc`, `:bcc`), and `:status` on success — never addresses, subject, or body. A
|
|
349
|
+
failed send raises through, so the event records the exception (and the send
|
|
350
|
+
still raises to your caller).
|
|
351
|
+
|
|
352
|
+
Outside Rails, plug in any object with an `instrument(name, payload) { ... }`
|
|
353
|
+
method (the same shape as `ActiveSupport::Notifications`):
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
Cloudflare::EmailService.configure do |c|
|
|
357
|
+
c.instrumenter = MyInstrumenter
|
|
358
|
+
end
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
279
363
|
## Development
|
|
280
364
|
|
|
281
365
|
```sh
|
|
@@ -15,6 +15,8 @@ module Cloudflare
|
|
|
15
15
|
# client.send_email(from: "a@x.com", to: "b@y.com",
|
|
16
16
|
# subject: "Hi", text: "Hello")
|
|
17
17
|
class Client
|
|
18
|
+
include Instrumentation
|
|
19
|
+
|
|
18
20
|
attr_reader :account_id, :api_token, :api_base, :open_timeout, :timeout
|
|
19
21
|
|
|
20
22
|
def initialize(account_id: nil, api_token: nil, api_base: nil,
|
|
@@ -40,7 +42,8 @@ module Cloudflare
|
|
|
40
42
|
# Sends a pre-built {Message}.
|
|
41
43
|
# @return [Response]
|
|
42
44
|
def deliver(message)
|
|
43
|
-
|
|
45
|
+
message.validate!
|
|
46
|
+
instrument_delivery(:rest, message) { post(send_uri, message.to_h) }
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
private
|
|
@@ -77,7 +80,15 @@ module Cloudflare
|
|
|
77
80
|
response = Response.new(status: status, body: parse(http_response.body))
|
|
78
81
|
return response if status.between?(200, 299) && response.success?
|
|
79
82
|
|
|
80
|
-
raise_error(status, response)
|
|
83
|
+
raise_error(status, response, retry_after: retry_after_from(http_response))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The `Retry-After` header as a non-negative Integer of seconds, or nil
|
|
87
|
+
# when absent or given as an HTTP-date rather than a delay.
|
|
88
|
+
def retry_after_from(http_response)
|
|
89
|
+
raw = http_response["retry-after"]
|
|
90
|
+
seconds = Integer(raw.to_s.strip, exception: false)
|
|
91
|
+
seconds if seconds&.>=(0)
|
|
81
92
|
end
|
|
82
93
|
|
|
83
94
|
def parse(raw)
|
|
@@ -88,12 +99,13 @@ module Cloudflare
|
|
|
88
99
|
{ "success" => false, "errors" => [{ "message" => "non-JSON response body" }] }
|
|
89
100
|
end
|
|
90
101
|
|
|
91
|
-
def raise_error(status, response)
|
|
102
|
+
def raise_error(status, response, retry_after: nil)
|
|
92
103
|
raise error_class_for(status).new(
|
|
93
104
|
summarize(response.errors),
|
|
94
105
|
status: status,
|
|
95
106
|
errors: response.errors,
|
|
96
107
|
response: response,
|
|
108
|
+
retry_after: retry_after,
|
|
97
109
|
)
|
|
98
110
|
end
|
|
99
111
|
|
|
@@ -34,6 +34,12 @@ module Cloudflare
|
|
|
34
34
|
# Cloudflare Email Worker signs with.
|
|
35
35
|
attr_accessor :ingress_secret
|
|
36
36
|
|
|
37
|
+
# @return [#instrument] receives a "deliver.cloudflare_email_service"
|
|
38
|
+
# event on each send. Defaults to ActiveSupport::Notifications when it is
|
|
39
|
+
# loaded, otherwise a no-op. Assign any object with an
|
|
40
|
+
# `instrument(name, payload) { ... }` method.
|
|
41
|
+
attr_writer :instrumenter
|
|
42
|
+
|
|
37
43
|
def initialize
|
|
38
44
|
@transport = ENV.fetch("CLOUDFLARE_EMAIL_TRANSPORT", "rest").to_sym
|
|
39
45
|
@account_id = ENV.fetch("CLOUDFLARE_ACCOUNT_ID", nil)
|
|
@@ -45,6 +51,24 @@ module Cloudflare
|
|
|
45
51
|
@timeout = DEFAULT_TIMEOUT
|
|
46
52
|
@ingress_secret = ENV.fetch("CLOUDFLARE_EMAIL_INGRESS_SECRET", nil)
|
|
47
53
|
end
|
|
54
|
+
|
|
55
|
+
# Resolved on each read (never memoized) until one is set explicitly, so
|
|
56
|
+
# ActiveSupport::Notifications is picked up as soon as it loads — even if
|
|
57
|
+
# an earlier send already resolved the default to the no-op. An explicit
|
|
58
|
+
# assignment always wins.
|
|
59
|
+
def instrumenter
|
|
60
|
+
@instrumenter || default_instrumenter
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def default_instrumenter
|
|
66
|
+
if defined?(ActiveSupport::Notifications)
|
|
67
|
+
ActiveSupport::Notifications
|
|
68
|
+
else
|
|
69
|
+
NullInstrumenter
|
|
70
|
+
end
|
|
71
|
+
end
|
|
48
72
|
end
|
|
49
73
|
end
|
|
50
74
|
end
|
|
@@ -19,11 +19,17 @@ module Cloudflare
|
|
|
19
19
|
attr_reader :errors
|
|
20
20
|
# @return [Response, nil] the wrapped API response.
|
|
21
21
|
attr_reader :response
|
|
22
|
+
# @return [Integer, nil] seconds to wait before retrying, parsed from the
|
|
23
|
+
# `Retry-After` response header whenever the API sends one (typically a
|
|
24
|
+
# 429, sometimes a 503). nil when the header is absent or not an integer
|
|
25
|
+
# number of seconds.
|
|
26
|
+
attr_reader :retry_after
|
|
22
27
|
|
|
23
|
-
def initialize(message = nil, status: nil, errors: nil, response: nil)
|
|
28
|
+
def initialize(message = nil, status: nil, errors: nil, response: nil, retry_after: nil)
|
|
24
29
|
@status = status
|
|
25
30
|
@errors = errors || []
|
|
26
31
|
@response = response
|
|
32
|
+
@retry_after = retry_after
|
|
27
33
|
super(message)
|
|
28
34
|
end
|
|
29
35
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudflare
|
|
4
|
+
module EmailService
|
|
5
|
+
# Fallback instrumenter used when ActiveSupport::Notifications is absent.
|
|
6
|
+
# Mirrors its `instrument(name, payload) { ... }` signature so the two are
|
|
7
|
+
# interchangeable, and simply runs the block.
|
|
8
|
+
module NullInstrumenter
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def instrument(_name, payload = {})
|
|
12
|
+
yield payload if block_given?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Shared send instrumentation for the REST and SMTP clients. Wraps each
|
|
17
|
+
# delivery in a `"deliver.cloudflare_email_service"` event published through
|
|
18
|
+
# the configured instrumenter (ActiveSupport::Notifications when present,
|
|
19
|
+
# otherwise a no-op).
|
|
20
|
+
#
|
|
21
|
+
# The payload carries the transport and recipient counts — never addresses,
|
|
22
|
+
# subject, or body — plus the response status on success. On failure the
|
|
23
|
+
# block raises through, so an ActiveSupport::Notifications instrumenter
|
|
24
|
+
# records the exception on the event.
|
|
25
|
+
module Instrumentation
|
|
26
|
+
EVENT = "deliver.cloudflare_email_service"
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def instrument_delivery(transport, message)
|
|
31
|
+
payload = {
|
|
32
|
+
transport: transport,
|
|
33
|
+
to: recipient_count(message.to),
|
|
34
|
+
cc: recipient_count(message.cc),
|
|
35
|
+
bcc: recipient_count(message.bcc),
|
|
36
|
+
}
|
|
37
|
+
EmailService.configuration.instrumenter.instrument(EVENT, payload) do
|
|
38
|
+
response = yield
|
|
39
|
+
payload[:status] = response.status
|
|
40
|
+
response
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def recipient_count(value)
|
|
45
|
+
return 0 if value.nil?
|
|
46
|
+
|
|
47
|
+
value.is_a?(Array) ? value.length : 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -13,6 +13,8 @@ module Cloudflare
|
|
|
13
13
|
# client.send_email(from: "a@x.com", to: "b@y.com",
|
|
14
14
|
# subject: "Hi", text: "Hello")
|
|
15
15
|
class SMTPClient
|
|
16
|
+
include Instrumentation
|
|
17
|
+
|
|
16
18
|
# Cloudflare requires the literal string "api_token" as the SMTP username;
|
|
17
19
|
# the password is the API token itself.
|
|
18
20
|
SMTP_USERNAME = "api_token"
|
|
@@ -44,19 +46,24 @@ module Cloudflare
|
|
|
44
46
|
# @return [Response]
|
|
45
47
|
def deliver(message)
|
|
46
48
|
message.validate!
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
# Map errors inside the instrumentation block (not around it) so a
|
|
50
|
+
# subscriber sees the same gem error classes the caller does — matching
|
|
51
|
+
# the REST transport, which raises its wrapped errors from within.
|
|
52
|
+
instrument_delivery(:smtp, message) do
|
|
53
|
+
envelope = build_envelope(message)
|
|
54
|
+
envelope.delivery_method(:smtp, smtp_settings)
|
|
55
|
+
transmit(envelope)
|
|
56
|
+
accepted(envelope)
|
|
57
|
+
rescue Net::SMTPAuthenticationError => e
|
|
58
|
+
raise AuthenticationError, e.message
|
|
59
|
+
rescue Net::SMTPError => e
|
|
60
|
+
# Every other SMTP protocol error (busy, syntax, fatal, unknown, ...).
|
|
61
|
+
raise ServerError, e.message
|
|
62
|
+
rescue Timeout::Error, Errno::ECONNREFUSED, Errno::ECONNRESET,
|
|
63
|
+
Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError, IOError,
|
|
64
|
+
OpenSSL::SSL::SSLError => e
|
|
65
|
+
raise NetworkError, "SMTP delivery failed: #{e.class}: #{e.message}"
|
|
66
|
+
end
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
private
|
|
@@ -5,6 +5,7 @@ require_relative "email_service/errors"
|
|
|
5
5
|
require_relative "email_service/configuration"
|
|
6
6
|
require_relative "email_service/message"
|
|
7
7
|
require_relative "email_service/response"
|
|
8
|
+
require_relative "email_service/instrumentation"
|
|
8
9
|
require_relative "email_service/client"
|
|
9
10
|
require_relative "email_service/smtp_client"
|
|
10
11
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
# Generator-only namespace. The flat constant maps the gem name
|
|
6
|
+
# `cloudflare-email_service` onto the `rails g cloudflare_email_service:install`
|
|
7
|
+
# command (Rails derives the namespace from the first module). Runtime code
|
|
8
|
+
# lives under `Cloudflare::EmailService`; this file is loaded only by Rails'
|
|
9
|
+
# generator lookup, never at gem runtime, so the core stays Rails-free.
|
|
10
|
+
module CloudflareEmailService
|
|
11
|
+
module Generators
|
|
12
|
+
# Creates the initializer and prints the remaining setup steps.
|
|
13
|
+
#
|
|
14
|
+
# bin/rails g cloudflare_email_service:install
|
|
15
|
+
class InstallGenerator < Rails::Generators::Base
|
|
16
|
+
source_root File.expand_path("templates", __dir__)
|
|
17
|
+
|
|
18
|
+
desc "Create a Cloudflare Email Service initializer and print the setup steps."
|
|
19
|
+
|
|
20
|
+
def create_initializer
|
|
21
|
+
template "cloudflare_email_service.rb",
|
|
22
|
+
"config/initializers/cloudflare_email_service.rb"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def print_next_steps
|
|
26
|
+
say "\ncloudflare-email_service installed.\n", :green
|
|
27
|
+
say <<~STEPS
|
|
28
|
+
Next steps:
|
|
29
|
+
|
|
30
|
+
1. Point ActionMailer at Cloudflare (e.g. config/environments/production.rb):
|
|
31
|
+
config.action_mailer.delivery_method = :cloudflare
|
|
32
|
+
|
|
33
|
+
2. Set credentials — in the initializer just created, or via the
|
|
34
|
+
environment: CLOUDFLARE_ACCOUNT_ID (REST only) and CLOUDFLARE_API_TOKEN.
|
|
35
|
+
|
|
36
|
+
3. Inbound mail (Action Mailbox), optional — in the initializer,
|
|
37
|
+
uncomment `ingress_secret` and the `to_prepare` require, then set:
|
|
38
|
+
config.action_mailbox.ingress = :cloudflare
|
|
39
|
+
and deploy the bundled Worker (find it at
|
|
40
|
+
`Cloudflare::EmailService.worker_template_path`), setting its
|
|
41
|
+
CLOUDFLARE_EMAIL_INGRESS_URL and CLOUDFLARE_EMAIL_INGRESS_SECRET vars.
|
|
42
|
+
STEPS
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Configure the Cloudflare Email Service client. Credentials can also come from
|
|
4
|
+
# the environment (CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN), in which case
|
|
5
|
+
# this block is optional.
|
|
6
|
+
Cloudflare::EmailService.configure do |c|
|
|
7
|
+
c.account_id = Rails.application.credentials.dig(:cloudflare, :account_id)
|
|
8
|
+
c.api_token = Rails.application.credentials.dig(:cloudflare, :api_token)
|
|
9
|
+
# c.transport = :smtp # optional; defaults to :rest (SMTP needs the `mail` gem)
|
|
10
|
+
|
|
11
|
+
# Inbound (Action Mailbox) only — must match the Worker's signing secret:
|
|
12
|
+
# c.ingress_secret = Rails.application.credentials.dig(:cloudflare, :ingress_secret)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Inbound (Action Mailbox) only — load the :cloudflare ingress. In `to_prepare`
|
|
16
|
+
# so its controller superclass is autoloadable regardless of boot order. Pair it
|
|
17
|
+
# with `config.action_mailbox.ingress = :cloudflare` in your environment config.
|
|
18
|
+
# Rails.application.config.to_prepare do
|
|
19
|
+
# require "cloudflare/email_service/action_mailbox"
|
|
20
|
+
# end
|
|
@@ -48,8 +48,19 @@ export default {
|
|
|
48
48
|
body: raw,
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
if (response.ok) return;
|
|
52
|
+
|
|
53
|
+
// 4xx: the app refused this message and a retry won't change the outcome
|
|
54
|
+
// (bad signature, wrong media type, unprocessable). Reject permanently so
|
|
55
|
+
// the sender gets a bounce instead of the message being silently dropped.
|
|
56
|
+
if (response.status >= 400 && response.status < 500) {
|
|
57
|
+
return message.setReject(`ingress rejected message (${response.status})`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 5xx (or any other non-2xx): a transient app problem — a deploy, a 502/503,
|
|
61
|
+
// a brief 500. Throw instead of setReject, exactly as a failed fetch() above
|
|
62
|
+
// already would, so the message is NOT permanently bounced and the sending
|
|
63
|
+
// server retries delivery once the app recovers.
|
|
64
|
+
throw new Error(`ingress temporarily unavailable (${response.status})`);
|
|
54
65
|
},
|
|
55
66
|
};
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cloudflare-email_service
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Elvinas Predkelis
|
|
@@ -121,9 +121,9 @@ dependencies:
|
|
|
121
121
|
- - "~>"
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
123
|
version: '3.0'
|
|
124
|
-
description:
|
|
125
|
-
dependencies
|
|
126
|
-
|
|
124
|
+
description: Send and receive email with the Cloudflare Email Service in Ruby. Zero
|
|
125
|
+
runtime dependencies, with optional ActionMailer and Action Mailbox adapters for
|
|
126
|
+
Rails.
|
|
127
127
|
email:
|
|
128
128
|
- elvinas@primevise.com
|
|
129
129
|
executables: []
|
|
@@ -139,12 +139,15 @@ files:
|
|
|
139
139
|
- lib/cloudflare/email_service/configuration.rb
|
|
140
140
|
- lib/cloudflare/email_service/errors.rb
|
|
141
141
|
- lib/cloudflare/email_service/inbound.rb
|
|
142
|
+
- lib/cloudflare/email_service/instrumentation.rb
|
|
142
143
|
- lib/cloudflare/email_service/message.rb
|
|
143
144
|
- lib/cloudflare/email_service/rails.rb
|
|
144
145
|
- lib/cloudflare/email_service/railtie.rb
|
|
145
146
|
- lib/cloudflare/email_service/response.rb
|
|
146
147
|
- lib/cloudflare/email_service/smtp_client.rb
|
|
147
148
|
- lib/cloudflare/email_service/version.rb
|
|
149
|
+
- lib/generators/cloudflare_email_service/install_generator.rb
|
|
150
|
+
- lib/generators/cloudflare_email_service/templates/cloudflare_email_service.rb
|
|
148
151
|
- templates/cloudflare_email_worker.js
|
|
149
152
|
homepage: https://github.com/elvinaspredkelis/cloudflare-email_service
|
|
150
153
|
licenses:
|