cloudflare-email_service 0.0.1 → 0.1.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 +30 -0
- data/README.md +129 -186
- data/lib/cloudflare/email_service/action_mailbox.rb +79 -0
- data/lib/cloudflare/email_service/client.rb +5 -3
- data/lib/cloudflare/email_service/inbound.rb +44 -0
- data/lib/cloudflare/email_service/rails.rb +17 -15
- data/lib/cloudflare/email_service/railtie.rb +22 -0
- data/lib/cloudflare/email_service/smtp_client.rb +5 -3
- data/lib/cloudflare/email_service/version.rb +1 -1
- data/lib/cloudflare/email_service.rb +14 -2
- data/templates/cloudflare_email_worker.js +41 -0
- metadata +54 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfedd8bad6300247c099669c46ecc795a2fb19faee035a1edf039717836a12b9
|
|
4
|
+
data.tar.gz: 44c03575adca32ba9e85ab037f140051fbc933afcaec13581ebcf3326df6d8ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c879194227a7591df1568a59857aba63d2d26b153db4ea7f4b0a5d3bb0ae38759be74429343403640e720a40b9a7c3782eeb6d6463f7796cf204980955032f3
|
|
7
|
+
data.tar.gz: 527048e68ef2e504228eeeb11df7efb55a8531fd0d11c92cf833b64a1c916e22d9feedb422636881695e4df41fb0ed89ead512a3f586e544ab6c826e1fc34ebe
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-06-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Optional, opt-in Action Mailbox ingress
|
|
13
|
+
(`require "cloudflare/email_service/action_mailbox"`) registering a
|
|
14
|
+
`:cloudflare` ingress at `POST /rails/action_mailbox/cloudflare/inbound_emails`.
|
|
15
|
+
A Cloudflare Email Worker forwards the raw RFC822 message, signed with
|
|
16
|
+
HMAC-SHA256 over `"<timestamp>.<body>"`; the ingress verifies the signature
|
|
17
|
+
(`CLOUDFLARE_EMAIL_INGRESS_SECRET` or the `cloudflare.ingress_secret`
|
|
18
|
+
credential) and rejects stale timestamps to block replays. The core gem stays
|
|
19
|
+
Rails-free; nothing loads unless the ingress is required.
|
|
20
|
+
- A ready-to-deploy Cloudflare Email Worker ships with the gem at
|
|
21
|
+
`templates/cloudflare_email_worker.js`; find it locally via
|
|
22
|
+
`Cloudflare::EmailService.worker_template_path`.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Require Ruby 3.2+ (Ruby 3.1 is end-of-life and the Rails 8 integration needs
|
|
26
|
+
3.2.2+).
|
|
27
|
+
- The `:cloudflare` ActionMailer delivery method now registers automatically
|
|
28
|
+
via a Railtie inside Rails — no `require "cloudflare/email_service/rails"`
|
|
29
|
+
needed. Set `config.action_mailer.delivery_method = :cloudflare` and go;
|
|
30
|
+
credentials are read from `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN`.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- `NetworkError` now also wraps TLS/SSL handshake failures
|
|
34
|
+
(`OpenSSL::SSL::SSLError`) on both transports, matching the documented error
|
|
35
|
+
contract.
|
|
36
|
+
|
|
7
37
|
## [0.0.1] - 2026-06-17
|
|
8
38
|
|
|
9
39
|
### Added
|
data/README.md
CHANGED
|
@@ -1,97 +1,38 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
[](https://github.com/elvinaspredkelis/cloudflare-email_service/actions/workflows/ci.yml)
|
|
1
|
+
# Cloudflare Email Service
|
|
4
2
|
|
|
5
3
|
A small Ruby client for sending transactional email through the
|
|
6
|
-
[Cloudflare Email Service](https://developers.cloudflare.com/email-service/)
|
|
7
|
-
over either of two transports:
|
|
4
|
+
[Cloudflare Email Service](https://developers.cloudflare.com/email-service/).
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
Two interchangeable transports: **REST** (default — zero dependencies, just
|
|
7
|
+
`net/http`) and **SMTP** (optional, via the [`mail`](https://rubygems.org/gems/mail)
|
|
8
|
+
gem). Same `send_email` call either way; pick the transport in configuration.
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send
|
|
14
|
-
```
|
|
10
|
+
Developed at [Primevise](https://primevise.com).
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
<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
|
+
<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>
|
|
14
|
+
<a href="https://rubygems.org/gems/cloudflare-email_service"><img alt="Gem Downloads" src="https://img.shields.io/gem/dt/cloudflare-email_service?color=10b981&include_prereleases&logo=ruby&logoColor=f43f5e"></a>
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
---
|
|
21
17
|
|
|
22
18
|
## Installation
|
|
23
19
|
|
|
24
|
-
Add it to your `Gemfile`:
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
gem "cloudflare-email_service"
|
|
28
|
-
|
|
29
|
-
# Only if you use the SMTP transport:
|
|
30
|
-
gem "mail"
|
|
31
20
|
```
|
|
32
|
-
|
|
33
|
-
Then run `bundle install`. Or install directly:
|
|
34
|
-
|
|
35
|
-
```sh
|
|
36
|
-
gem install cloudflare-email_service
|
|
21
|
+
bundle add cloudflare-email_service
|
|
37
22
|
```
|
|
38
23
|
|
|
39
|
-
Requires Ruby 3.
|
|
40
|
-
|
|
41
|
-
## Credentials
|
|
42
|
-
|
|
43
|
-
You need a Cloudflare **API token**, plus an **account id** for the REST
|
|
44
|
-
transport. Token scope depends on the transport:
|
|
45
|
-
|
|
46
|
-
| Transport | account id | API token scope |
|
|
47
|
-
| --------- | ---------- | ---------------------- |
|
|
48
|
-
| REST | required | `Email Sending: Send` |
|
|
49
|
-
| SMTP | not used | `Email Sending: Edit` |
|
|
50
|
-
|
|
51
|
-
Provide them through the environment:
|
|
24
|
+
Requires Ruby 3.2+. For the SMTP transport, also add the `mail` gem:
|
|
52
25
|
|
|
53
|
-
```sh
|
|
54
|
-
export CLOUDFLARE_ACCOUNT_ID="your-account-id" # REST only
|
|
55
|
-
export CLOUDFLARE_API_TOKEN="your-api-token"
|
|
56
|
-
export CLOUDFLARE_EMAIL_TRANSPORT="rest" # or "smtp" (default: rest)
|
|
57
26
|
```
|
|
58
|
-
|
|
59
|
-
or explicitly in code (see below).
|
|
60
|
-
|
|
61
|
-
## Choosing a transport
|
|
62
|
-
|
|
63
|
-
The transport is selected once, in configuration; everything else — the
|
|
64
|
-
`send_email` call, the returned `Response`, the error classes — is identical.
|
|
65
|
-
|
|
66
|
-
```ruby
|
|
67
|
-
# REST (default) — zero dependencies
|
|
68
|
-
Cloudflare::EmailService.configure do |config|
|
|
69
|
-
config.transport = :rest
|
|
70
|
-
config.account_id = ENV["CLOUDFLARE_ACCOUNT_ID"]
|
|
71
|
-
config.api_token = ENV["CLOUDFLARE_API_TOKEN"]
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# SMTP — requires the `mail` gem
|
|
75
|
-
Cloudflare::EmailService.configure do |config|
|
|
76
|
-
config.transport = :smtp
|
|
77
|
-
config.api_token = ENV["CLOUDFLARE_API_TOKEN"] # account_id not needed
|
|
78
|
-
end
|
|
27
|
+
bundle add mail
|
|
79
28
|
```
|
|
80
29
|
|
|
81
|
-
|
|
82
|
-
`config.smtp_host` / `config.smtp_port` if needed. If you select `:smtp` without
|
|
83
|
-
the `mail` gem installed, a `ConfigurationError` is raised telling you to add it.
|
|
84
|
-
|
|
85
|
-
You can also build a transport client directly:
|
|
86
|
-
|
|
87
|
-
```ruby
|
|
88
|
-
Cloudflare::EmailService::Client.new(account_id: "...", api_token: "...") # REST
|
|
89
|
-
Cloudflare::EmailService::SMTPClient.new(api_token: "...") # SMTP
|
|
90
|
-
```
|
|
30
|
+
---
|
|
91
31
|
|
|
92
32
|
## Usage
|
|
93
33
|
|
|
94
|
-
|
|
34
|
+
Configure once with your Cloudflare API token (plus an account id for REST),
|
|
35
|
+
then send:
|
|
95
36
|
|
|
96
37
|
```ruby
|
|
97
38
|
require "cloudflare/email_service"
|
|
@@ -99,7 +40,6 @@ require "cloudflare/email_service"
|
|
|
99
40
|
Cloudflare::EmailService.configure do |config|
|
|
100
41
|
config.account_id = ENV["CLOUDFLARE_ACCOUNT_ID"]
|
|
101
42
|
config.api_token = ENV["CLOUDFLARE_API_TOKEN"]
|
|
102
|
-
config.timeout = 30 # seconds (optional)
|
|
103
43
|
end
|
|
104
44
|
|
|
105
45
|
response = Cloudflare::EmailService.send_email(
|
|
@@ -114,91 +54,87 @@ response.success? # => true
|
|
|
114
54
|
response.delivered # => ["recipient@example.com"]
|
|
115
55
|
```
|
|
116
56
|
|
|
117
|
-
|
|
57
|
+
`Response` also exposes `#queued`, `#permanent_bounces`, `#errors`, `#status`,
|
|
58
|
+
and the raw parsed `#body`.
|
|
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>" }`.
|
|
118
66
|
|
|
119
|
-
|
|
120
|
-
|
|
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:
|
|
121
72
|
|
|
122
73
|
```ruby
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
74
|
+
Cloudflare::EmailService::Client.new(account_id: "...", api_token: "...") # REST
|
|
75
|
+
Cloudflare::EmailService::SMTPClient.new(api_token: "...") # SMTP
|
|
76
|
+
```
|
|
127
77
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
78
|
+
#### Credentials
|
|
79
|
+
|
|
80
|
+
Set credentials in `configure` (above) or through the environment:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
export CLOUDFLARE_ACCOUNT_ID="your-account-id" # REST only
|
|
84
|
+
export CLOUDFLARE_API_TOKEN="your-api-token"
|
|
134
85
|
```
|
|
135
86
|
|
|
136
|
-
|
|
87
|
+
REST needs an `Email Sending: Send` token; SMTP needs `Email Sending: Edit` and
|
|
88
|
+
no account id.
|
|
137
89
|
|
|
138
|
-
|
|
90
|
+
---
|
|
139
91
|
|
|
140
|
-
|
|
141
|
-
- a hash — `{ email: "user@example.com", name: "Display Name" }`
|
|
142
|
-
(`:address` is accepted as an alias for `:email`)
|
|
143
|
-
- `to` / `cc` / `bcc` also accept an array of any of the above
|
|
92
|
+
## Transports
|
|
144
93
|
|
|
145
|
-
|
|
146
|
-
Cloudflare
|
|
147
|
-
|
|
148
|
-
to: ["a@example.com", { email: "b@example.com", name: "B" }],
|
|
149
|
-
cc: "team@yourdomain.com",
|
|
150
|
-
reply_to: "support@yourdomain.com",
|
|
151
|
-
subject: "Hi",
|
|
152
|
-
text: "Hello",
|
|
153
|
-
)
|
|
154
|
-
```
|
|
94
|
+
Both transports accept the same `send_email` call and return the same
|
|
95
|
+
`Response` — they differ only in how the message reaches Cloudflare. Choose one
|
|
96
|
+
with `config.transport`.
|
|
155
97
|
|
|
156
|
-
|
|
98
|
+
**REST** (`:rest`, the default) posts JSON to the Cloudflare API over HTTPS
|
|
99
|
+
using only `net/http` from the standard library — no MIME assembly, no gems.
|
|
100
|
+
Needs an `account_id` and an `Email Sending: Send` token. The right default for
|
|
101
|
+
almost everything.
|
|
157
102
|
|
|
158
|
-
|
|
159
|
-
|
|
103
|
+
**SMTP** (`:smtp`) submits over `smtp.mx.cloudflare.net:465` (implicit TLS), with
|
|
104
|
+
MIME built by the [`mail`](https://rubygems.org/gems/mail) gem — loaded lazily,
|
|
105
|
+
only when this transport is used. Needs an `Email Sending: Edit` token and no
|
|
106
|
+
account id. Reach for it when your environment already speaks SMTP or only
|
|
107
|
+
allows SMTP egress.
|
|
160
108
|
|
|
161
109
|
```ruby
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
to: "recipient@example.com",
|
|
167
|
-
subject: "Your report",
|
|
168
|
-
text: "See attached.",
|
|
169
|
-
attachments: [
|
|
170
|
-
{
|
|
171
|
-
content: Base64.strict_encode64(File.read("report.pdf")),
|
|
172
|
-
filename: "report.pdf",
|
|
173
|
-
type: "application/pdf",
|
|
174
|
-
disposition: "attachment", # optional
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
)
|
|
110
|
+
Cloudflare::EmailService.configure do |config|
|
|
111
|
+
config.transport = :smtp # default: :rest
|
|
112
|
+
config.api_token = ENV["CLOUDFLARE_API_TOKEN"]
|
|
113
|
+
end
|
|
178
114
|
```
|
|
179
115
|
|
|
180
|
-
|
|
116
|
+
> [!NOTE]
|
|
117
|
+
> Selecting `:smtp` without the `mail` gem installed raises a
|
|
118
|
+
> `ConfigurationError` telling you to add it.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Rails
|
|
123
|
+
|
|
124
|
+
Adding the gem registers a `:cloudflare` delivery method automatically — just
|
|
125
|
+
point ActionMailer at it. No require, no initializer:
|
|
181
126
|
|
|
182
127
|
```ruby
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
to: "b@example.com",
|
|
186
|
-
subject: "Re: thread",
|
|
187
|
-
text: "Reply body",
|
|
188
|
-
headers: { "In-Reply-To" => "<msg-123@yourdomain.com>" },
|
|
189
|
-
)
|
|
128
|
+
# config/environments/production.rb
|
|
129
|
+
config.action_mailer.delivery_method = :cloudflare
|
|
190
130
|
```
|
|
191
131
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
when you require it — it registers a `:cloudflare` ActionMailer delivery method
|
|
196
|
-
backed by whichever transport you configured.
|
|
132
|
+
Credentials come from `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN` in the
|
|
133
|
+
environment. To set them in code (or pick the SMTP transport), add an
|
|
134
|
+
initializer:
|
|
197
135
|
|
|
198
136
|
```ruby
|
|
199
137
|
# config/initializers/cloudflare_email_service.rb
|
|
200
|
-
require "cloudflare/email_service/rails"
|
|
201
|
-
|
|
202
138
|
Cloudflare::EmailService.configure do |c|
|
|
203
139
|
c.account_id = Rails.application.credentials.dig(:cloudflare, :account_id)
|
|
204
140
|
c.api_token = Rails.application.credentials.dig(:cloudflare, :api_token)
|
|
@@ -206,80 +142,87 @@ Cloudflare::EmailService.configure do |c|
|
|
|
206
142
|
end
|
|
207
143
|
```
|
|
208
144
|
|
|
145
|
+
Your mailers then send through Cloudflare unchanged. Prefer ActionMailer's
|
|
146
|
+
built-in `:smtp` delivery? Point it at Cloudflare with the settings helper:
|
|
147
|
+
|
|
209
148
|
```ruby
|
|
210
|
-
|
|
211
|
-
config.action_mailer.
|
|
149
|
+
config.action_mailer.delivery_method = :smtp
|
|
150
|
+
config.action_mailer.smtp_settings = Cloudflare::EmailService.smtp_settings(
|
|
151
|
+
api_token: Rails.application.credentials.dig(:cloudflare, :api_token),
|
|
152
|
+
)
|
|
212
153
|
```
|
|
213
154
|
|
|
214
|
-
|
|
155
|
+
### Inbound email (Action Mailbox)
|
|
156
|
+
|
|
157
|
+
Receive mail too. Cloudflare delivers inbound mail to an app only through an
|
|
158
|
+
[Email Worker](https://developers.cloudflare.com/email-routing/email-workers/),
|
|
159
|
+
so first enable
|
|
160
|
+
[Email Routing](https://developers.cloudflare.com/email-service/get-started/route-emails/)
|
|
161
|
+
on your domain (it adds the MX/SPF/DKIM records). Then a Worker forwards each
|
|
162
|
+
message to a `:cloudflare`
|
|
163
|
+
[Action Mailbox](https://guides.rubyonrails.org/action_mailbox_basics.html)
|
|
164
|
+
ingress that ships with the gem. Three steps:
|
|
165
|
+
|
|
166
|
+
**1. Require the ingress** — opt-in, so it stays out of send-only apps:
|
|
215
167
|
|
|
216
168
|
```ruby
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
mail(from: "welcome@yourdomain.com", to: user.email, subject: "Welcome")
|
|
220
|
-
end
|
|
221
|
-
end
|
|
169
|
+
# config/initializers/cloudflare_email_service.rb
|
|
170
|
+
require "cloudflare/email_service/action_mailbox"
|
|
222
171
|
```
|
|
223
172
|
|
|
224
|
-
|
|
225
|
-
|
|
173
|
+
**2. Select it** and set the shared signing secret — either
|
|
174
|
+
`CLOUDFLARE_EMAIL_INGRESS_SECRET` or the `cloudflare.ingress_secret` credential:
|
|
226
175
|
|
|
227
176
|
```ruby
|
|
228
|
-
config.
|
|
229
|
-
config.
|
|
230
|
-
api_token: Rails.application.credentials.dig(:cloudflare, :api_token),
|
|
231
|
-
)
|
|
177
|
+
# config/environments/production.rb
|
|
178
|
+
config.action_mailbox.ingress = :cloudflare
|
|
232
179
|
```
|
|
233
180
|
|
|
234
|
-
|
|
181
|
+
The route `POST /rails/action_mailbox/cloudflare/inbound_emails` is registered
|
|
182
|
+
for you, and every request is verified by an HMAC-SHA256 signature with replay
|
|
183
|
+
protection.
|
|
184
|
+
|
|
185
|
+
**3. Deploy an Email Worker** that signs and forwards each message, and bind it
|
|
186
|
+
to an Email Routing rule (or catch-all). One ships with the gem — set your app
|
|
187
|
+
URL and give it the same `CLOUDFLARE_EMAIL_INGRESS_SECRET`, then deploy:
|
|
235
188
|
|
|
236
|
-
|
|
189
|
+
- In this repo: [`templates/cloudflare_email_worker.js`](templates/cloudflare_email_worker.js)
|
|
190
|
+
- From the installed gem: `Cloudflare::EmailService.worker_template_path`
|
|
237
191
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
| `#permanent_bounces` | array of permanently bounced addresses |
|
|
244
|
-
| `#errors` | array of Cloudflare error objects |
|
|
245
|
-
| `#status` | the HTTP status code |
|
|
246
|
-
| `#body` | the raw parsed JSON body |
|
|
192
|
+
> [!NOTE]
|
|
193
|
+
> The Worker sends `Content-Type: message/rfc822`; the ingress rejects anything
|
|
194
|
+
> else with `415 Unsupported Media Type`.
|
|
195
|
+
|
|
196
|
+
---
|
|
247
197
|
|
|
248
198
|
## Errors
|
|
249
199
|
|
|
250
|
-
Non-2xx responses (and unsuccessful payloads) raise a typed error
|
|
251
|
-
|
|
200
|
+
Non-2xx responses (and unsuccessful payloads) raise a typed error — every one a
|
|
201
|
+
subclass of `Cloudflare::EmailService::Error`:
|
|
252
202
|
|
|
253
|
-
| Class
|
|
254
|
-
|
|
|
255
|
-
| `ConfigurationError`
|
|
256
|
-
| `ValidationError`
|
|
257
|
-
| `AuthenticationError`
|
|
258
|
-
| `RequestError`
|
|
259
|
-
| `RateLimitError`
|
|
260
|
-
| `ServerError`
|
|
261
|
-
| `NetworkError`
|
|
203
|
+
| Class | When |
|
|
204
|
+
| --------------------- | ------------------------------------------ |
|
|
205
|
+
| `ConfigurationError` | missing `account_id` / `api_token` |
|
|
206
|
+
| `ValidationError` | the message is missing required fields |
|
|
207
|
+
| `AuthenticationError` | HTTP 401 / 403 |
|
|
208
|
+
| `RequestError` | HTTP 400 / 422 and other 4xx |
|
|
209
|
+
| `RateLimitError` | HTTP 429 |
|
|
210
|
+
| `ServerError` | HTTP 5xx |
|
|
211
|
+
| `NetworkError` | connection, timeout, and TLS failures |
|
|
262
212
|
|
|
263
|
-
API errors carry
|
|
213
|
+
API errors also carry `#status` and `#errors` for context.
|
|
264
214
|
|
|
265
|
-
|
|
266
|
-
begin
|
|
267
|
-
Cloudflare::EmailService.send_email(...)
|
|
268
|
-
rescue Cloudflare::EmailService::APIError => e
|
|
269
|
-
e.status # => 403
|
|
270
|
-
e.errors # => [{ "code" => 10000, "message" => "Authentication error" }]
|
|
271
|
-
e.message # => "[10000] Authentication error"
|
|
272
|
-
end
|
|
273
|
-
```
|
|
215
|
+
---
|
|
274
216
|
|
|
275
217
|
## Development
|
|
276
218
|
|
|
277
219
|
```sh
|
|
278
|
-
bundle install
|
|
279
220
|
bundle exec rake test # run the Minitest suite
|
|
280
221
|
bundle exec rubocop # lint
|
|
281
222
|
```
|
|
282
223
|
|
|
224
|
+
---
|
|
225
|
+
|
|
283
226
|
## License
|
|
284
227
|
|
|
285
228
|
Released under the [MIT License](LICENSE.txt).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cloudflare/email_service"
|
|
4
|
+
require "cloudflare/email_service/inbound"
|
|
5
|
+
|
|
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 explicitly
|
|
8
|
+
# and select it with `config.action_mailbox.ingress = :cloudflare`. The route is
|
|
9
|
+
# registered automatically. See the README for setup and the Worker snippet.
|
|
10
|
+
if defined?(ActionMailbox)
|
|
11
|
+
module ActionMailbox
|
|
12
|
+
module Ingresses
|
|
13
|
+
module Cloudflare
|
|
14
|
+
# Receives the raw RFC822 message a Cloudflare Email Worker forwards and
|
|
15
|
+
# hands it to Action Mailbox for routing. The Worker signs each request
|
|
16
|
+
# (HMAC-SHA256 over "<timestamp>.<body>"); we verify the signature and
|
|
17
|
+
# reject stale timestamps before accepting the message.
|
|
18
|
+
class InboundEmailsController < ActionMailbox::BaseController
|
|
19
|
+
before_action :verify_signature, :require_valid_rfc822_message
|
|
20
|
+
|
|
21
|
+
def create
|
|
22
|
+
if raw_body.empty?
|
|
23
|
+
head :unprocessable_entity
|
|
24
|
+
else
|
|
25
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id!(raw_body)
|
|
26
|
+
head :no_content
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def verify_signature
|
|
33
|
+
case ::Cloudflare::EmailService::Inbound.verify(
|
|
34
|
+
secret: signing_secret,
|
|
35
|
+
timestamp: request.headers["X-CF-Email-Timestamp"],
|
|
36
|
+
signature: request.headers["X-CF-Email-Signature"],
|
|
37
|
+
body: raw_body,
|
|
38
|
+
)
|
|
39
|
+
when :ok then nil
|
|
40
|
+
when :stale then head :request_timeout
|
|
41
|
+
else head :unauthorized
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def require_valid_rfc822_message
|
|
46
|
+
return if request.media_type == "message/rfc822"
|
|
47
|
+
|
|
48
|
+
head :unsupported_media_type
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Read once, as binary, so the bytes match exactly what the Worker
|
|
52
|
+
# signed and what Action Mailbox stores.
|
|
53
|
+
def raw_body
|
|
54
|
+
@raw_body ||= begin
|
|
55
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
|
56
|
+
request.body.read.to_s.b
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Shared HMAC secret, from the environment or Rails credentials.
|
|
61
|
+
def signing_secret
|
|
62
|
+
ENV["CLOUDFLARE_EMAIL_INGRESS_SECRET"] ||
|
|
63
|
+
::Rails.application.credentials.dig(:cloudflare, :ingress_secret).to_s
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Register the route so it works like the built-in ingresses. Guarded for
|
|
71
|
+
# non-Rails requires; add the route by hand if your boot order skips this.
|
|
72
|
+
if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
73
|
+
Rails.application.routes.append do
|
|
74
|
+
post "/rails/action_mailbox/cloudflare/inbound_emails" =>
|
|
75
|
+
"action_mailbox/ingresses/cloudflare/inbound_emails#create",
|
|
76
|
+
as: :rails_cloudflare_inbound_emails
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "net/http"
|
|
4
|
+
require "openssl"
|
|
4
5
|
require "json"
|
|
5
6
|
require "uri"
|
|
6
7
|
|
|
@@ -32,8 +33,8 @@ module Cloudflare
|
|
|
32
33
|
# Builds and sends a message. Accepts the same keyword arguments as
|
|
33
34
|
# {Message#initialize}.
|
|
34
35
|
# @return [Response]
|
|
35
|
-
def send_email(**
|
|
36
|
-
deliver(Message.new(**
|
|
36
|
+
def send_email(**)
|
|
37
|
+
deliver(Message.new(**))
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
# Sends a pre-built {Message}.
|
|
@@ -58,7 +59,8 @@ module Cloudflare
|
|
|
58
59
|
|
|
59
60
|
handle(http(uri).request(request))
|
|
60
61
|
rescue Timeout::Error, Errno::ECONNREFUSED, Errno::ECONNRESET,
|
|
61
|
-
Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError, IOError
|
|
62
|
+
Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError, IOError,
|
|
63
|
+
OpenSSL::SSL::SSLError => e
|
|
62
64
|
raise NetworkError, "request failed: #{e.class}: #{e.message}"
|
|
63
65
|
end
|
|
64
66
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Cloudflare
|
|
6
|
+
module EmailService
|
|
7
|
+
# Verifies the HMAC-SHA256 signature a Cloudflare Email Worker attaches to a
|
|
8
|
+
# forwarded inbound message. The Worker signs `"<timestamp>.<raw body>"` with
|
|
9
|
+
# a shared secret and sends the timestamp and hex digest as headers; this
|
|
10
|
+
# recomputes the digest, compares it in constant time, and rejects stale
|
|
11
|
+
# timestamps to block replays.
|
|
12
|
+
#
|
|
13
|
+
# Pure and Rails-free (stdlib OpenSSL only) so it can be unit-tested on its
|
|
14
|
+
# own; the Action Mailbox ingress is a thin wrapper around {.verify}.
|
|
15
|
+
module Inbound
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Reject timestamps more than this many seconds from now (either side).
|
|
19
|
+
REPLAY_WINDOW = 300
|
|
20
|
+
|
|
21
|
+
# @return [Symbol] :ok, :stale (timestamp outside the window), or
|
|
22
|
+
# :bad_signature (missing/empty input or digest mismatch).
|
|
23
|
+
def verify(secret:, timestamp:, signature:, body:, now: Time.now.to_i)
|
|
24
|
+
return :bad_signature if [secret, timestamp, signature, body].any? { |v| v.to_s.empty? }
|
|
25
|
+
return :stale if (now - timestamp.to_i).abs > REPLAY_WINDOW
|
|
26
|
+
|
|
27
|
+
# Build the signed payload in binary: raw RFC822 bodies carry bytes > 127
|
|
28
|
+
# (8bit transfer encoding, binary attachments), which would raise
|
|
29
|
+
# Encoding::CompatibilityError if interpolated into a UTF-8 string.
|
|
30
|
+
signed = "#{timestamp}.".b + body.to_s.b
|
|
31
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret.to_s, signed)
|
|
32
|
+
secure_compare(expected, signature.to_s) ? :ok : :bad_signature
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Constant-time string comparison. Bails early on a length mismatch, which
|
|
36
|
+
# the digest's fixed width makes safe to leak.
|
|
37
|
+
def secure_compare(expected, actual)
|
|
38
|
+
return false unless expected.bytesize == actual.bytesize
|
|
39
|
+
|
|
40
|
+
OpenSSL.fixed_length_secure_compare(expected, actual)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -4,22 +4,21 @@ require "cloudflare/email_service"
|
|
|
4
4
|
|
|
5
5
|
module Cloudflare
|
|
6
6
|
module EmailService
|
|
7
|
-
#
|
|
7
|
+
# Rails / ActionMailer integration. Registers a `:cloudflare` delivery
|
|
8
|
+
# method backed by the configured transport.
|
|
8
9
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# backed by the configured transport:
|
|
10
|
+
# Inside a Rails app this loads automatically via {Railtie} — just set the
|
|
11
|
+
# delivery method and credentials; no require needed:
|
|
12
12
|
#
|
|
13
|
-
# # config/
|
|
14
|
-
#
|
|
13
|
+
# # config/environments/production.rb
|
|
14
|
+
# config.action_mailer.delivery_method = :cloudflare
|
|
15
15
|
#
|
|
16
|
+
# # credentials come from ENV (CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN)
|
|
17
|
+
# # or an initializer:
|
|
16
18
|
# Cloudflare::EmailService.configure do |c|
|
|
17
19
|
# c.account_id = Rails.application.credentials.dig(:cloudflare, :account_id)
|
|
18
20
|
# c.api_token = Rails.application.credentials.dig(:cloudflare, :api_token)
|
|
19
21
|
# end
|
|
20
|
-
#
|
|
21
|
-
# # config/environments/production.rb
|
|
22
|
-
# config.action_mailer.delivery_method = :cloudflare
|
|
23
22
|
module Rails
|
|
24
23
|
# Converts an ActionMailer-built Mail::Message into the keyword arguments
|
|
25
24
|
# accepted by {Message#initialize}.
|
|
@@ -124,13 +123,16 @@ module Cloudflare
|
|
|
124
123
|
end
|
|
125
124
|
|
|
126
125
|
# Register the delivery method when ActionMailer is present. Guarded so this
|
|
127
|
-
# file is safe to require in a non-Rails process.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
elsif defined?(ActionMailer::Base)
|
|
126
|
+
# file is safe to require in a non-Rails process. ActionMailer::Base is checked
|
|
127
|
+
# first so that when the Railtie requires this inside an `on_load(:action_mailer)`
|
|
128
|
+
# hook (mailer already loaded), registration happens immediately rather than
|
|
129
|
+
# scheduling a second, nested hook that may not run.
|
|
130
|
+
if defined?(ActionMailer::Base)
|
|
133
131
|
ActionMailer::Base.add_delivery_method(
|
|
134
132
|
:cloudflare, Cloudflare::EmailService::Rails::DeliveryMethod
|
|
135
133
|
)
|
|
134
|
+
elsif defined?(ActiveSupport)
|
|
135
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
136
|
+
add_delivery_method :cloudflare, Cloudflare::EmailService::Rails::DeliveryMethod
|
|
137
|
+
end
|
|
136
138
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module Cloudflare
|
|
6
|
+
module EmailService
|
|
7
|
+
# Registers the `:cloudflare` ActionMailer delivery method automatically
|
|
8
|
+
# inside a Rails app, so adding the gem is enough — no explicit require.
|
|
9
|
+
#
|
|
10
|
+
# Loaded only when Rails is present (see cloudflare/email_service.rb), so the
|
|
11
|
+
# core gem stays Rails-free. Registration is inert until a host opts in with
|
|
12
|
+
# `config.action_mailer.delivery_method = :cloudflare`.
|
|
13
|
+
class Railtie < ::Rails::Railtie
|
|
14
|
+
initializer "cloudflare_email_service.action_mailer",
|
|
15
|
+
before: "action_mailer.set_configs" do
|
|
16
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
17
|
+
require "cloudflare/email_service/rails"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -36,8 +36,8 @@ module Cloudflare
|
|
|
36
36
|
# Builds and sends a message. Accepts the same keyword arguments as
|
|
37
37
|
# {Message#initialize}.
|
|
38
38
|
# @return [Response]
|
|
39
|
-
def send_email(**
|
|
40
|
-
deliver(Message.new(**
|
|
39
|
+
def send_email(**)
|
|
40
|
+
deliver(Message.new(**))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Sends a pre-built {Message} over SMTP.
|
|
@@ -54,7 +54,8 @@ module Cloudflare
|
|
|
54
54
|
# Every other SMTP protocol error (busy, syntax, fatal, unknown, ...).
|
|
55
55
|
raise ServerError, e.message
|
|
56
56
|
rescue Timeout::Error, Errno::ECONNREFUSED, Errno::ECONNRESET,
|
|
57
|
-
Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError, IOError
|
|
57
|
+
Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError, IOError,
|
|
58
|
+
OpenSSL::SSL::SSLError => e
|
|
58
59
|
raise NetworkError, "SMTP delivery failed: #{e.class}: #{e.message}"
|
|
59
60
|
end
|
|
60
61
|
|
|
@@ -148,6 +149,7 @@ module Cloudflare
|
|
|
148
149
|
def load_dependencies!
|
|
149
150
|
require "mail"
|
|
150
151
|
require "net/smtp"
|
|
152
|
+
require "openssl"
|
|
151
153
|
rescue LoadError => e
|
|
152
154
|
raise ConfigurationError,
|
|
153
155
|
"the SMTP transport needs the `mail` gem — add `gem \"mail\"` " \
|
|
@@ -51,8 +51,16 @@ module Cloudflare
|
|
|
51
51
|
# Convenience: build a {Client} from the global config and send.
|
|
52
52
|
# Accepts the same keyword arguments as {Message#initialize}.
|
|
53
53
|
# @return [Response]
|
|
54
|
-
def send_email(**
|
|
55
|
-
client.send_email(**
|
|
54
|
+
def send_email(**)
|
|
55
|
+
client.send_email(**)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Absolute path to the shipped Cloudflare Email Worker template that signs
|
|
59
|
+
# and forwards inbound mail to the Action Mailbox ingress. Set your app URL
|
|
60
|
+
# and secret in it, then deploy it.
|
|
61
|
+
# @return [String]
|
|
62
|
+
def worker_template_path
|
|
63
|
+
File.expand_path("../../templates/cloudflare_email_worker.js", __dir__)
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
# Cloudflare SMTP submission settings, in the shape both {SMTPClient} and
|
|
@@ -77,3 +85,7 @@ module Cloudflare
|
|
|
77
85
|
end
|
|
78
86
|
end
|
|
79
87
|
end
|
|
88
|
+
|
|
89
|
+
# Auto-wire the ActionMailer delivery method when running inside Rails. Guarded
|
|
90
|
+
# so the core gem stays Rails-free everywhere else.
|
|
91
|
+
require_relative "email_service/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Cloudflare Email Worker for cloudflare-email_service inbound (Action Mailbox).
|
|
2
|
+
//
|
|
3
|
+
// Forwards each received message to your Rails app's :cloudflare ingress, signed
|
|
4
|
+
// with HMAC-SHA256 over "<timestamp>.<body>" so the ingress can verify it and
|
|
5
|
+
// reject replays.
|
|
6
|
+
//
|
|
7
|
+
// Setup:
|
|
8
|
+
// 1. Replace the URL below with your app's host.
|
|
9
|
+
// 2. Set CLOUDFLARE_EMAIL_INGRESS_SECRET as a Worker secret, matching the
|
|
10
|
+
// app's CLOUDFLARE_EMAIL_INGRESS_SECRET (or cloudflare.ingress_secret).
|
|
11
|
+
// 3. Bind the Worker to your Email Routing rule and deploy (e.g. wrangler).
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
async email(message, env) {
|
|
15
|
+
// arrayBuffer (not text) preserves the raw bytes of non-UTF-8 messages.
|
|
16
|
+
const raw = new Uint8Array(await new Response(message.raw).arrayBuffer());
|
|
17
|
+
const ts = Math.floor(Date.now() / 1000).toString();
|
|
18
|
+
|
|
19
|
+
const key = await crypto.subtle.importKey(
|
|
20
|
+
"raw", new TextEncoder().encode(env.CLOUDFLARE_EMAIL_INGRESS_SECRET),
|
|
21
|
+
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"],
|
|
22
|
+
);
|
|
23
|
+
const signed = new Uint8Array([...new TextEncoder().encode(ts + "."), ...raw]);
|
|
24
|
+
const digest = await crypto.subtle.sign("HMAC", key, signed);
|
|
25
|
+
const sig = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
|
|
27
|
+
const response = await fetch("https://your-app.example.com/rails/action_mailbox/cloudflare/inbound_emails", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "message/rfc822",
|
|
31
|
+
"X-CF-Email-Timestamp": ts,
|
|
32
|
+
"X-CF-Email-Signature": sig,
|
|
33
|
+
},
|
|
34
|
+
body: raw,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Reject on failure so Cloudflare bounces the message rather than silently
|
|
38
|
+
// accepting (and dropping) it.
|
|
39
|
+
if (!response.ok) message.setReject(`ingress error ${response.status}`);
|
|
40
|
+
},
|
|
41
|
+
};
|
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.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Elvinas Predkelis
|
|
@@ -9,6 +9,34 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actionmailbox
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: actionmailer
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
12
40
|
- !ruby/object:Gem::Dependency
|
|
13
41
|
name: mail
|
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +65,20 @@ dependencies:
|
|
|
37
65
|
- - "~>"
|
|
38
66
|
- !ruby/object:Gem::Version
|
|
39
67
|
version: '5.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: railties
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '7.1'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '7.1'
|
|
40
82
|
- !ruby/object:Gem::Dependency
|
|
41
83
|
name: rake
|
|
42
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,11 +121,11 @@ dependencies:
|
|
|
79
121
|
- - "~>"
|
|
80
122
|
- !ruby/object:Gem::Version
|
|
81
123
|
version: '3.0'
|
|
82
|
-
description: A small Ruby client for
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
description: A small Ruby client for the Cloudflare Email Service with zero runtime
|
|
125
|
+
dependencies and optional Rails integration (ActionMailer delivery and Action Mailbox
|
|
126
|
+
inbound).
|
|
85
127
|
email:
|
|
86
|
-
- elvinas@
|
|
128
|
+
- elvinas@primevise.com
|
|
87
129
|
executables: []
|
|
88
130
|
extensions: []
|
|
89
131
|
extra_rdoc_files: []
|
|
@@ -92,14 +134,18 @@ files:
|
|
|
92
134
|
- LICENSE.txt
|
|
93
135
|
- README.md
|
|
94
136
|
- lib/cloudflare/email_service.rb
|
|
137
|
+
- lib/cloudflare/email_service/action_mailbox.rb
|
|
95
138
|
- lib/cloudflare/email_service/client.rb
|
|
96
139
|
- lib/cloudflare/email_service/configuration.rb
|
|
97
140
|
- lib/cloudflare/email_service/errors.rb
|
|
141
|
+
- lib/cloudflare/email_service/inbound.rb
|
|
98
142
|
- lib/cloudflare/email_service/message.rb
|
|
99
143
|
- lib/cloudflare/email_service/rails.rb
|
|
144
|
+
- lib/cloudflare/email_service/railtie.rb
|
|
100
145
|
- lib/cloudflare/email_service/response.rb
|
|
101
146
|
- lib/cloudflare/email_service/smtp_client.rb
|
|
102
147
|
- lib/cloudflare/email_service/version.rb
|
|
148
|
+
- templates/cloudflare_email_worker.js
|
|
103
149
|
homepage: https://github.com/elvinaspredkelis/cloudflare-email_service
|
|
104
150
|
licenses:
|
|
105
151
|
- MIT
|
|
@@ -114,14 +160,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
114
160
|
requirements:
|
|
115
161
|
- - ">="
|
|
116
162
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '3.
|
|
163
|
+
version: '3.2'
|
|
118
164
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
165
|
requirements:
|
|
120
166
|
- - ">="
|
|
121
167
|
- !ruby/object:Gem::Version
|
|
122
168
|
version: '0'
|
|
123
169
|
requirements: []
|
|
124
|
-
rubygems_version: 3.6.
|
|
170
|
+
rubygems_version: 3.6.9
|
|
125
171
|
specification_version: 4
|
|
126
|
-
summary: Send email through the Cloudflare Email Service
|
|
172
|
+
summary: Send and receive email through the Cloudflare Email Service.
|
|
127
173
|
test_files: []
|