cloudflare-email_service 0.1.0 → 0.2.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 +21 -0
- data/README.md +114 -44
- data/lib/cloudflare/email_service/action_mailbox.rb +12 -11
- data/lib/cloudflare/email_service/configuration.rb +5 -0
- data/lib/cloudflare/email_service/version.rb +1 -1
- data/templates/cloudflare_email_worker.js +24 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae0f87f99efee9db13b22ae4cd2560f75878ee141ce0c34797da94dbb387a37c
|
|
4
|
+
data.tar.gz: 23cd36dc8c771b45a649d93d6d9385ab76751755b72ffcd795840280c359a53f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5d2e2efb6a2fa418400b2b0d4f26558ef3a647f880d89c81ee71ba1a278d7182ccb8c0db68116a9b106284fb0587b8ebac96a6ba0678aa3da61e7ac6b97b910
|
|
7
|
+
data.tar.gz: c1db350ec8e6323eb9b7b9f3b5db198c1cf1801c3f790567d0d53c8f34c29ed7d9e2108a9601d9ad9c5b4ba7d9076bcee627856961cbdfb7ea2725f875484b7d
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-06-19
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- The bundled Email Worker template now answers `GET /` with a
|
|
13
|
+
`{ ok, configured }` health check, so the worker URL no longer errors and you
|
|
14
|
+
can confirm its vars are set.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- The bundled Email Worker template reads the ingress URL from a
|
|
18
|
+
`CLOUDFLARE_EMAIL_INGRESS_URL` Worker var instead of a hardcoded URL, so it
|
|
19
|
+
deploys unchanged across apps/environments.
|
|
20
|
+
- The inbound ingress secret is now `config.ingress_secret` on the gem
|
|
21
|
+
configuration (defaulting from `CLOUDFLARE_EMAIL_INGRESS_SECRET`), set in the
|
|
22
|
+
same `configure` block as the other credentials — instead of an ad-hoc env /
|
|
23
|
+
Rails-credentials lookup in the controller.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Read the inbound request body via `request.raw_post`, which is consistent
|
|
27
|
+
across Puma, Falcon, and Unicorn and nil-safe. The previous `request.body.read`
|
|
28
|
+
raised on servers (e.g. Falcon) that hand back no body object.
|
|
29
|
+
|
|
9
30
|
## [0.1.0] - 2026-06-18
|
|
10
31
|
|
|
11
32
|
### Added
|
data/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# Cloudflare Email Service
|
|
2
2
|
|
|
3
|
-
A small Ruby client for
|
|
4
|
-
|
|
3
|
+
A small Ruby client for the [Cloudflare Email Service](https://developers.cloudflare.com/email-service/):
|
|
4
|
+
send transactional email from any Ruby app, and — in Rails — receive it through
|
|
5
|
+
Action Mailbox.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
`net/http`)
|
|
8
|
-
gem).
|
|
7
|
+
Sending uses one of two interchangeable transports: **REST** (the default — zero
|
|
8
|
+
dependencies, just `net/http`) or **SMTP** (optional, via the
|
|
9
|
+
[`mail`](https://rubygems.org/gems/mail) gem). The same `send_email` call works
|
|
10
|
+
for both.
|
|
9
11
|
|
|
10
|
-
Developed at [Primevise](https://primevise.com).
|
|
12
|
+
Battle-tested at [Rinkta](https://rinkta.com). Developed at [Primevise](https://primevise.com).
|
|
11
13
|
|
|
12
14
|
<a href="https://github.com/elvinaspredkelis/cloudflare-email_service/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/elvinaspredkelis/cloudflare-email_service/actions/workflows/ci.yml/badge.svg"></a>
|
|
13
15
|
<a href="https://rubygems.org/gems/cloudflare-email_service"><img alt="Gem Version" src="https://img.shields.io/gem/v/cloudflare-email_service?color=10b981&include_prereleases&logo=ruby&logoColor=f43f5e"></a>
|
|
@@ -17,19 +19,19 @@ Developed at [Primevise](https://primevise.com).
|
|
|
17
19
|
|
|
18
20
|
## Installation
|
|
19
21
|
|
|
20
|
-
```
|
|
22
|
+
```sh
|
|
21
23
|
bundle add cloudflare-email_service
|
|
22
24
|
```
|
|
23
25
|
|
|
24
26
|
Requires Ruby 3.2+. For the SMTP transport, also add the `mail` gem:
|
|
25
27
|
|
|
26
|
-
```
|
|
28
|
+
```sh
|
|
27
29
|
bundle add mail
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
32
|
-
##
|
|
34
|
+
## Quick start
|
|
33
35
|
|
|
34
36
|
Configure once with your Cloudflare API token (plus an account id for REST),
|
|
35
37
|
then send:
|
|
@@ -54,28 +56,15 @@ response.success? # => true
|
|
|
54
56
|
response.delivered # => ["recipient@example.com"]
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
> [!TIP]
|
|
61
|
-
> `from`, `to`, `cc`, `bcc`, and `reply_to` accept a string
|
|
62
|
-
> (`"a@x.com"` or `"Display Name <a@x.com>"`), a hash (`{ email:, name: }`), or —
|
|
63
|
-
> for `to` / `cc` / `bcc` — an array of either. Add files with
|
|
64
|
-
> `attachments: [{ content: Base64.strict_encode64(bytes), filename:, type: }]`
|
|
65
|
-
> and arbitrary headers with `headers: { "In-Reply-To" => "<id>" }`.
|
|
66
|
-
|
|
67
|
-
> [!CAUTION]
|
|
68
|
-
> The total message size (body + attachments) must not exceed **5 MiB**.
|
|
69
|
-
|
|
70
|
-
Skip the global config and pass credentials per client when you send from more
|
|
71
|
-
than one account:
|
|
59
|
+
Need to send from more than one account? Skip the global config and build a
|
|
60
|
+
client directly:
|
|
72
61
|
|
|
73
62
|
```ruby
|
|
74
63
|
Cloudflare::EmailService::Client.new(account_id: "...", api_token: "...") # REST
|
|
75
64
|
Cloudflare::EmailService::SMTPClient.new(api_token: "...") # SMTP
|
|
76
65
|
```
|
|
77
66
|
|
|
78
|
-
|
|
67
|
+
### Credentials
|
|
79
68
|
|
|
80
69
|
Set credentials in `configure` (above) or through the environment:
|
|
81
70
|
|
|
@@ -89,11 +78,50 @@ no account id.
|
|
|
89
78
|
|
|
90
79
|
---
|
|
91
80
|
|
|
81
|
+
## Messages
|
|
82
|
+
|
|
83
|
+
`send_email` accepts these keywords (`from`, `to`, `subject`, and one of
|
|
84
|
+
`html` / `text` are required):
|
|
85
|
+
|
|
86
|
+
| Keyword | Description |
|
|
87
|
+
| ------- | ----------- |
|
|
88
|
+
| `from` | Sender. A string or `{ email:, name: }` hash. |
|
|
89
|
+
| `to` / `cc` / `bcc` | Recipients. A string, a hash, or an array of either. |
|
|
90
|
+
| `reply_to` | Reply-To address (string or hash). |
|
|
91
|
+
| `subject` | Subject line. |
|
|
92
|
+
| `html` / `text` | Body. Provide either or both. |
|
|
93
|
+
| `attachments` | Array of `{ content:, filename:, type:, disposition: }`. |
|
|
94
|
+
| `headers` | Hash of custom headers, e.g. `{ "In-Reply-To" => "<id>" }`. |
|
|
95
|
+
|
|
96
|
+
In an address hash, `:address` aliases `:email`. Attachment `content` is Base64:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
Cloudflare::EmailService.send_email(
|
|
100
|
+
from: "reports@yourdomain.com",
|
|
101
|
+
to: "recipient@example.com",
|
|
102
|
+
subject: "Your report",
|
|
103
|
+
text: "See attached.",
|
|
104
|
+
attachments: [
|
|
105
|
+
{
|
|
106
|
+
content: Base64.strict_encode64(File.read("report.pdf")),
|
|
107
|
+
filename: "report.pdf",
|
|
108
|
+
type: "application/pdf",
|
|
109
|
+
disposition: "attachment", # optional
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
> [!CAUTION]
|
|
116
|
+
> The total message size (body + attachments) must not exceed **5 MiB**.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
92
120
|
## Transports
|
|
93
121
|
|
|
94
122
|
Both transports accept the same `send_email` call and return the same
|
|
95
|
-
`Response` — they differ only in how the message reaches Cloudflare.
|
|
96
|
-
with `config.transport`.
|
|
123
|
+
[`Response`](#response) — they differ only in how the message reaches Cloudflare.
|
|
124
|
+
Choose one with `config.transport`.
|
|
97
125
|
|
|
98
126
|
**REST** (`:rest`, the default) posts JSON to the Cloudflare API over HTTPS
|
|
99
127
|
using only `net/http` from the standard library — no MIME assembly, no gems.
|
|
@@ -130,8 +158,8 @@ config.action_mailer.delivery_method = :cloudflare
|
|
|
130
158
|
```
|
|
131
159
|
|
|
132
160
|
Credentials come from `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN` in the
|
|
133
|
-
environment. To set them in code
|
|
134
|
-
initializer:
|
|
161
|
+
environment. To set them in code, choose the SMTP transport, or receive inbound
|
|
162
|
+
mail, use a single initializer:
|
|
135
163
|
|
|
136
164
|
```ruby
|
|
137
165
|
# config/initializers/cloudflare_email_service.rb
|
|
@@ -139,6 +167,15 @@ Cloudflare::EmailService.configure do |c|
|
|
|
139
167
|
c.account_id = Rails.application.credentials.dig(:cloudflare, :account_id)
|
|
140
168
|
c.api_token = Rails.application.credentials.dig(:cloudflare, :api_token)
|
|
141
169
|
# c.transport = :smtp # optional; defaults to :rest
|
|
170
|
+
|
|
171
|
+
# Inbound (Action Mailbox) only — must match the Worker's signing secret:
|
|
172
|
+
c.ingress_secret = Rails.application.credentials.dig(:cloudflare, :ingress_secret)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Inbound (Action Mailbox) only — load the :cloudflare ingress. In `to_prepare`
|
|
176
|
+
# so its controller superclass is autoloadable regardless of boot order.
|
|
177
|
+
Rails.application.config.to_prepare do
|
|
178
|
+
require "cloudflare/email_service/action_mailbox"
|
|
142
179
|
end
|
|
143
180
|
```
|
|
144
181
|
|
|
@@ -161,17 +198,10 @@ so first enable
|
|
|
161
198
|
on your domain (it adds the MX/SPF/DKIM records). Then a Worker forwards each
|
|
162
199
|
message to a `:cloudflare`
|
|
163
200
|
[Action Mailbox](https://guides.rubyonrails.org/action_mailbox_basics.html)
|
|
164
|
-
ingress that ships with the gem.
|
|
201
|
+
ingress that ships with the gem. The initializer above loads the ingress and
|
|
202
|
+
sets the signing secret; then:
|
|
165
203
|
|
|
166
|
-
**1.
|
|
167
|
-
|
|
168
|
-
```ruby
|
|
169
|
-
# config/initializers/cloudflare_email_service.rb
|
|
170
|
-
require "cloudflare/email_service/action_mailbox"
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
**2. Select it** and set the shared signing secret — either
|
|
174
|
-
`CLOUDFLARE_EMAIL_INGRESS_SECRET` or the `cloudflare.ingress_secret` credential:
|
|
204
|
+
**1. Select the ingress:**
|
|
175
205
|
|
|
176
206
|
```ruby
|
|
177
207
|
# config/environments/production.rb
|
|
@@ -180,21 +210,42 @@ config.action_mailbox.ingress = :cloudflare
|
|
|
180
210
|
|
|
181
211
|
The route `POST /rails/action_mailbox/cloudflare/inbound_emails` is registered
|
|
182
212
|
for you, and every request is verified by an HMAC-SHA256 signature with replay
|
|
183
|
-
protection.
|
|
213
|
+
protection. The ingress reads the body via `request.raw_post`, so it works under
|
|
214
|
+
any Rack server — Puma, Falcon, or Unicorn.
|
|
184
215
|
|
|
185
|
-
**
|
|
186
|
-
to an Email Routing rule (or catch-all). One ships with the gem —
|
|
187
|
-
|
|
216
|
+
**2. Deploy an Email Worker** that signs and forwards each message, and bind it
|
|
217
|
+
to an Email Routing rule (or catch-all). One ships with the gem — deploy it
|
|
218
|
+
unchanged and set two Worker vars: `CLOUDFLARE_EMAIL_INGRESS_URL` (the route
|
|
219
|
+
above) and `CLOUDFLARE_EMAIL_INGRESS_SECRET` (matching the app):
|
|
188
220
|
|
|
189
221
|
- In this repo: [`templates/cloudflare_email_worker.js`](templates/cloudflare_email_worker.js)
|
|
190
222
|
- From the installed gem: `Cloudflare::EmailService.worker_template_path`
|
|
191
223
|
|
|
224
|
+
Visiting the worker's URL returns a `{ ok, configured }` health check — a quick
|
|
225
|
+
way to confirm it's deployed and both vars are set.
|
|
226
|
+
|
|
192
227
|
> [!NOTE]
|
|
193
228
|
> The Worker sends `Content-Type: message/rfc822`; the ingress rejects anything
|
|
194
229
|
> else with `415 Unsupported Media Type`.
|
|
195
230
|
|
|
196
231
|
---
|
|
197
232
|
|
|
233
|
+
## Response
|
|
234
|
+
|
|
235
|
+
`send_email` returns a `Cloudflare::EmailService::Response`:
|
|
236
|
+
|
|
237
|
+
| Method | Returns |
|
|
238
|
+
| -------------------- | --------------------------------------------- |
|
|
239
|
+
| `#success?` | `true` when Cloudflare accepted the request |
|
|
240
|
+
| `#delivered` | array of accepted recipient addresses |
|
|
241
|
+
| `#queued` | array of queued recipient addresses |
|
|
242
|
+
| `#permanent_bounces` | array of permanently bounced addresses |
|
|
243
|
+
| `#errors` | array of Cloudflare error objects |
|
|
244
|
+
| `#status` | HTTP status code |
|
|
245
|
+
| `#body` | the raw parsed JSON body |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
198
249
|
## Errors
|
|
199
250
|
|
|
200
251
|
Non-2xx responses (and unsuccessful payloads) raise a typed error — every one a
|
|
@@ -210,19 +261,38 @@ subclass of `Cloudflare::EmailService::Error`:
|
|
|
210
261
|
| `ServerError` | HTTP 5xx |
|
|
211
262
|
| `NetworkError` | connection, timeout, and TLS failures |
|
|
212
263
|
|
|
213
|
-
API errors
|
|
264
|
+
The API errors (`AuthenticationError`, `RequestError`, `RateLimitError`,
|
|
265
|
+
`ServerError`) inherit from `APIError` and carry `#status` and `#errors`:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
begin
|
|
269
|
+
Cloudflare::EmailService.send_email(from: "a@x.com", to: "b@y.com", subject: "Hi", text: "Hello")
|
|
270
|
+
rescue Cloudflare::EmailService::APIError => e
|
|
271
|
+
e.status # => 403
|
|
272
|
+
e.errors # => [{ "code" => 10000, "message" => "Authentication error" }]
|
|
273
|
+
e.message # => "[10000] Authentication error"
|
|
274
|
+
end
|
|
275
|
+
```
|
|
214
276
|
|
|
215
277
|
---
|
|
216
278
|
|
|
217
279
|
## Development
|
|
218
280
|
|
|
219
281
|
```sh
|
|
282
|
+
bundle install # install dependencies
|
|
220
283
|
bundle exec rake test # run the Minitest suite
|
|
221
284
|
bundle exec rubocop # lint
|
|
222
285
|
```
|
|
223
286
|
|
|
224
287
|
---
|
|
225
288
|
|
|
289
|
+
## Contributing
|
|
290
|
+
|
|
291
|
+
Bug reports and pull requests welcome on
|
|
292
|
+
[GitHub](https://github.com/elvinaspredkelis/cloudflare-email_service).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
226
296
|
## License
|
|
227
297
|
|
|
228
298
|
Released under the [MIT License](LICENSE.txt).
|
|
@@ -4,9 +4,11 @@ require "cloudflare/email_service"
|
|
|
4
4
|
require "cloudflare/email_service/inbound"
|
|
5
5
|
|
|
6
6
|
# Opt-in Action Mailbox ingress: forwards inbound mail from a Cloudflare Email
|
|
7
|
-
# Worker into Action Mailbox. Not loaded by the core gem — require it
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Worker into Action Mailbox. Not loaded by the core gem — require it from an
|
|
8
|
+
# initializer (inside `Rails.application.config.to_prepare`, so the controller's
|
|
9
|
+
# superclass is autoloadable) and select it with
|
|
10
|
+
# `config.action_mailbox.ingress = :cloudflare`. The route is registered
|
|
11
|
+
# automatically. See the README for setup and the Worker snippet.
|
|
10
12
|
if defined?(ActionMailbox)
|
|
11
13
|
module ActionMailbox
|
|
12
14
|
module Ingresses
|
|
@@ -49,18 +51,17 @@ if defined?(ActionMailbox)
|
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
# Read once, as binary, so the bytes match exactly what the Worker
|
|
52
|
-
# signed and what Action Mailbox stores.
|
|
54
|
+
# signed and what Action Mailbox stores. `raw_post` is ActionDispatch's
|
|
55
|
+
# body reader — consistent across Puma, Falcon, and Unicorn, and nil-safe
|
|
56
|
+
# (`request.body` can be nil/non-rewindable on some servers).
|
|
53
57
|
def raw_body
|
|
54
|
-
@raw_body ||=
|
|
55
|
-
request.body.rewind if request.body.respond_to?(:rewind)
|
|
56
|
-
request.body.read.to_s.b
|
|
57
|
-
end
|
|
58
|
+
@raw_body ||= request.raw_post.to_s.b
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
# Shared HMAC secret, from the
|
|
61
|
+
# Shared HMAC secret, from the gem configuration
|
|
62
|
+
# (`config.ingress_secret` / CLOUDFLARE_EMAIL_INGRESS_SECRET).
|
|
61
63
|
def signing_secret
|
|
62
|
-
|
|
63
|
-
::Rails.application.credentials.dig(:cloudflare, :ingress_secret).to_s
|
|
64
|
+
::Cloudflare::EmailService.configuration.ingress_secret.to_s
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
end
|
|
@@ -29,6 +29,10 @@ module Cloudflare
|
|
|
29
29
|
attr_accessor :open_timeout
|
|
30
30
|
# @return [Integer] read timeout in seconds.
|
|
31
31
|
attr_accessor :timeout
|
|
32
|
+
# @return [String, nil] HMAC secret used to verify inbound Action Mailbox
|
|
33
|
+
# requests (CLOUDFLARE_EMAIL_INGRESS_SECRET). Must match the value the
|
|
34
|
+
# Cloudflare Email Worker signs with.
|
|
35
|
+
attr_accessor :ingress_secret
|
|
32
36
|
|
|
33
37
|
def initialize
|
|
34
38
|
@transport = ENV.fetch("CLOUDFLARE_EMAIL_TRANSPORT", "rest").to_sym
|
|
@@ -39,6 +43,7 @@ module Cloudflare
|
|
|
39
43
|
@smtp_port = Integer(ENV.fetch("CLOUDFLARE_SMTP_PORT", DEFAULT_SMTP_PORT))
|
|
40
44
|
@open_timeout = DEFAULT_TIMEOUT
|
|
41
45
|
@timeout = DEFAULT_TIMEOUT
|
|
46
|
+
@ingress_secret = ENV.fetch("CLOUDFLARE_EMAIL_INGRESS_SECRET", nil)
|
|
42
47
|
end
|
|
43
48
|
end
|
|
44
49
|
end
|
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
// Cloudflare Email Worker for cloudflare-email_service inbound (Action Mailbox).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// with HMAC-SHA256 over "<timestamp>.<body>"
|
|
5
|
-
//
|
|
3
|
+
// - email(): forwards each received message to your Rails app's :cloudflare
|
|
4
|
+
// ingress, signed with HMAC-SHA256 over "<timestamp>.<body>".
|
|
5
|
+
// - fetch(): a GET health check. Email Workers have no HTTP API, so visiting the
|
|
6
|
+
// worker URL would otherwise error; this returns whether the vars are set.
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// Set two Worker secrets/vars, then bind the Worker to an Email Routing rule and
|
|
9
|
+
// deploy (e.g. wrangler):
|
|
10
|
+
// - CLOUDFLARE_EMAIL_INGRESS_URL full URL of the ingress, e.g.
|
|
11
|
+
// https://your-app.example.com/rails/action_mailbox/cloudflare/inbound_emails
|
|
12
|
+
// - CLOUDFLARE_EMAIL_INGRESS_SECRET same value as the app's
|
|
13
|
+
// config.ingress_secret (CLOUDFLARE_EMAIL_INGRESS_SECRET)
|
|
12
14
|
|
|
13
15
|
export default {
|
|
16
|
+
async fetch(_request, env) {
|
|
17
|
+
const configured = Boolean(
|
|
18
|
+
env.CLOUDFLARE_EMAIL_INGRESS_URL && env.CLOUDFLARE_EMAIL_INGRESS_SECRET,
|
|
19
|
+
);
|
|
20
|
+
return Response.json({ ok: true, configured });
|
|
21
|
+
},
|
|
22
|
+
|
|
14
23
|
async email(message, env) {
|
|
24
|
+
const { CLOUDFLARE_EMAIL_INGRESS_URL: url, CLOUDFLARE_EMAIL_INGRESS_SECRET: secret } = env;
|
|
25
|
+
if (!url || !secret) {
|
|
26
|
+
return message.setReject("CLOUDFLARE_EMAIL_INGRESS_URL / _SECRET not configured");
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
// arrayBuffer (not text) preserves the raw bytes of non-UTF-8 messages.
|
|
16
30
|
const raw = new Uint8Array(await new Response(message.raw).arrayBuffer());
|
|
17
31
|
const ts = Math.floor(Date.now() / 1000).toString();
|
|
18
32
|
|
|
19
33
|
const key = await crypto.subtle.importKey(
|
|
20
|
-
"raw", new TextEncoder().encode(
|
|
34
|
+
"raw", new TextEncoder().encode(secret),
|
|
21
35
|
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"],
|
|
22
36
|
);
|
|
23
37
|
const signed = new Uint8Array([...new TextEncoder().encode(ts + "."), ...raw]);
|
|
24
38
|
const digest = await crypto.subtle.sign("HMAC", key, signed);
|
|
25
39
|
const sig = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
26
40
|
|
|
27
|
-
const response = await fetch(
|
|
41
|
+
const response = await fetch(url, {
|
|
28
42
|
method: "POST",
|
|
29
43
|
headers: {
|
|
30
44
|
"Content-Type": "message/rfc822",
|