sendio 1.0.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 +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE +22 -0
- data/NOTICE +1 -0
- data/README.md +210 -0
- data/app/controllers/action_mailbox/ingresses/sendio/inbound_emails_controller.rb +95 -0
- data/lib/sendio/action_mailbox/engine.rb +19 -0
- data/lib/sendio/action_mailbox/message_builder.rb +162 -0
- data/lib/sendio/action_mailbox.rb +5 -0
- data/lib/sendio/api_keys.rb +32 -0
- data/lib/sendio/automations/runs.rb +22 -0
- data/lib/sendio/automations/templates.rb +29 -0
- data/lib/sendio/automations.rb +64 -0
- data/lib/sendio/batch.rb +36 -0
- data/lib/sendio/broadcasts.rb +180 -0
- data/lib/sendio/client.rb +19 -0
- data/lib/sendio/contact_properties.rb +106 -0
- data/lib/sendio/contacts/imports.rb +64 -0
- data/lib/sendio/contacts/segments.rb +66 -0
- data/lib/sendio/contacts/topics.rb +80 -0
- data/lib/sendio/contacts.rb +131 -0
- data/lib/sendio/domains.rb +60 -0
- data/lib/sendio/emails/attachments.rb +67 -0
- data/lib/sendio/emails/receiving/attachments.rb +69 -0
- data/lib/sendio/emails/receiving.rb +51 -0
- data/lib/sendio/emails.rb +124 -0
- data/lib/sendio/errors.rb +59 -0
- data/lib/sendio/events.rb +46 -0
- data/lib/sendio/logs.rb +20 -0
- data/lib/sendio/mailboxes.rb +87 -0
- data/lib/sendio/mailer.rb +241 -0
- data/lib/sendio/multipart_request.rb +67 -0
- data/lib/sendio/oauth_grants.rb +20 -0
- data/lib/sendio/pagination_helper.rb +30 -0
- data/lib/sendio/railtie.rb +14 -0
- data/lib/sendio/request.rb +122 -0
- data/lib/sendio/response.rb +154 -0
- data/lib/sendio/segments.rb +38 -0
- data/lib/sendio/suppressions/batch.rb +44 -0
- data/lib/sendio/suppressions.rb +64 -0
- data/lib/sendio/templates.rb +50 -0
- data/lib/sendio/topics.rb +38 -0
- data/lib/sendio/usage.rb +14 -0
- data/lib/sendio/version.rb +5 -0
- data/lib/sendio/webhooks.rb +329 -0
- data/lib/sendio.rb +78 -0
- metadata +119 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3b82f47130797ce62b2a01860ba0b682f084aad772203c5e82085fe6b3f4ecf3
|
|
4
|
+
data.tar.gz: bb2edfd4bd16576f826c46f48d5745a60481b321609e2cec6338c4aafa9c9bab
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aaaf1d140cdf58e068bc5b21d54865e202afbaee0921361c30d686faa8c8d5f72217b0dfd499679e0036e33cea5f100ecfb6f620d28a612894d2fb5e8e073769
|
|
7
|
+
data.tar.gz: cfca683fc0f253d84325d0319d76c86c0a3e077548ce35619bfbe3c56f26571d3ef2993ff493b581a2ee06f97401028159d32def6e8a750395e6b8345b38b031
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-11-11
|
|
9
|
+
|
|
10
|
+
First public release.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
|
|
14
|
+
- `Sendio::Domains::Claims` (`.create`, `.get`, `.verify`): the API has no domain-claim routes, so every call failed with a 404. Claim a domain by deleting it from the team that owns it, then adding it to yours.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `Sendio::Automations::Templates.list` (optional `category:` filter) and `.get(slug)` to browse the automation template catalog, and `Sendio::Automations.create_from_template(params, options: { idempotency_key: })` to create a disabled automation from a template.
|
|
19
|
+
- `Sendio::Mailboxes` with `.list`, `.create`, `.update`, `.reset_password` and `.remove`, to manage mailboxes (email accounts with an IMAP/SMTP login).
|
|
20
|
+
- `Sendio::Broadcasts.estimate`, `.test`, `.stats`, `.list_versions`, `.get_version`, `.update_version` and `.restore_version`.
|
|
21
|
+
- Documented `capabilities: { receiving: "enabled" | "disabled" }` on `Sendio::Domains.create` and `Sendio::Domains.update` to turn email receiving on or off for a domain (turning it on asks for the domain's MX record, publish it to start receiving email; sending can't be disabled).
|
|
22
|
+
- Documented the full set of broadcast create/update fields (`segment_ids`, `exclude_segment_ids`, `audience`, `unsubscribe_scope`, `tracking`, `template_engine`, `topic_id`, `preview_text`) and the `status`, `segment_id` and `search` list filters.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `Sendio::Broadcasts.update` and `.send` no longer send `broadcast_id` in the JSON body; it goes only in the path (the API rejects any key but `name` when updating a sent broadcast).
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- First release of the official send.io Ruby SDK (`sendio` gem).
|
|
31
|
+
- Default API base URL `https://api.send.io`, overridable with `SENDIO_BASE_URL` or `Sendio.base_url = "..."` at runtime (trailing slash optional).
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Derich Pacheco and Resend
|
|
4
|
+
Copyright (c) 2026 send.io
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
data/NOTICE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This SDK is based on https://github.com/resend/resend-ruby (MIT License), Copyright (c) Resend.
|
data/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# send.io Ruby and Rails SDK
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://badge.fury.io/rb/sendio)
|
|
5
|
+
|
|
6
|
+
The official Ruby and Rails SDK for the [send.io](https://send.io) email API. Full API reference: <https://developer.send.io/docs/api>.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Via RubyGems:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
gem install sendio
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Via Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "sendio"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Ruby 3.2 or newer.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
Create an API key in the send.io dashboard (keys start with `mx_`), then:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
require "sendio"
|
|
30
|
+
|
|
31
|
+
Sendio.api_key = ENV["SENDIO_API_KEY"]
|
|
32
|
+
|
|
33
|
+
email = Sendio::Emails.send({
|
|
34
|
+
from: "Acme <onboarding@yourdomain.com>",
|
|
35
|
+
to: ["delivered@send.io"],
|
|
36
|
+
subject: "Hello from send.io",
|
|
37
|
+
html: "<strong>It works!</strong>"
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
puts email[:id]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Send a batch
|
|
44
|
+
|
|
45
|
+
Up to 100 emails in one request:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
Sendio::Batch.send([
|
|
49
|
+
{
|
|
50
|
+
from: "Acme <onboarding@yourdomain.com>",
|
|
51
|
+
to: ["delivered@send.io"],
|
|
52
|
+
subject: "First",
|
|
53
|
+
html: "<p>one</p>"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
from: "Acme <onboarding@yourdomain.com>",
|
|
57
|
+
to: ["delivered@send.io"],
|
|
58
|
+
subject: "Second",
|
|
59
|
+
html: "<p>two</p>"
|
|
60
|
+
}
|
|
61
|
+
])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Retrieve an email
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
email = Sendio::Emails.get("4ef9a417-02e9-4d39-ad75-9611e0fcc33c")
|
|
68
|
+
puts email[:last_event]
|
|
69
|
+
|
|
70
|
+
Sendio::Emails.list(limit: 10)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Domains
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
domain = Sendio::Domains.create({ name: "yourdomain.com" })
|
|
77
|
+
Sendio::Domains.verify(domain[:id])
|
|
78
|
+
Sendio::Domains.get(domain[:id])
|
|
79
|
+
Sendio::Domains.list
|
|
80
|
+
Sendio::Domains.remove(domain[:id])
|
|
81
|
+
|
|
82
|
+
# Receive email on the domain too (send.io then requires its MX record)
|
|
83
|
+
domain = Sendio::Domains.create({ name: "yourdomain.com", capabilities: { receiving: "enabled" } })
|
|
84
|
+
|
|
85
|
+
# Stop receiving: send.io stops accepting email for the domain; remove its MX record
|
|
86
|
+
Sendio::Domains.update({ id: domain[:id], capabilities: { receiving: "disabled" } })
|
|
87
|
+
|
|
88
|
+
# Turn it back on (asks for the domain's MX record; publish it to start receiving email)
|
|
89
|
+
Sendio::Domains.update({ id: domain[:id], capabilities: { receiving: "enabled" } })
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Mailboxes
|
|
93
|
+
|
|
94
|
+
Mailboxes are real email accounts with an IMAP/SMTP login. The domain must be verified.
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
mailbox = Sendio::Mailboxes.create({ address: "support@yourdomain.com" })
|
|
98
|
+
puts mailbox[:credentials] # shown once; the password is also an API key for this mailbox
|
|
99
|
+
|
|
100
|
+
Sendio::Mailboxes.list
|
|
101
|
+
Sendio::Mailboxes.update({ mailbox_id: mailbox[:id], warmup_enabled: false })
|
|
102
|
+
Sendio::Mailboxes.reset_password(mailbox[:id]) # the old password stops working
|
|
103
|
+
Sendio::Mailboxes.remove(mailbox[:id]) # also removes an agent inbox on it
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Sending is always on; `capabilities: { sending: "disabled" }` is rejected with a 422.
|
|
107
|
+
|
|
108
|
+
The SDK also covers API keys, segments, contacts, contact properties, topics, broadcasts, templates, webhooks, received emails, logs, suppressions and more. See the [examples folder](examples/) for one script per resource.
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
### API key
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
Sendio.api_key = ENV["SENDIO_API_KEY"]
|
|
116
|
+
|
|
117
|
+
# or
|
|
118
|
+
Sendio.configure do |config|
|
|
119
|
+
config.api_key = ENV["SENDIO_API_KEY"]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# or lazily, evaluated on every request
|
|
123
|
+
Sendio.api_key = -> { Current.user.sendio_api_key }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Base URL
|
|
127
|
+
|
|
128
|
+
Requests go to `https://api.send.io` by default. Override it (for testing or a different environment) with the `SENDIO_BASE_URL` environment variable or at runtime. A trailing slash is optional:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# environment: SENDIO_BASE_URL=https://api.example.test
|
|
132
|
+
Sendio.base_url = "https://api.example.test"
|
|
133
|
+
Sendio.base_url = nil # back to SENDIO_BASE_URL or the default
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Error handling
|
|
137
|
+
|
|
138
|
+
Non-2xx responses raise a subclass of `Sendio::Error`:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
begin
|
|
142
|
+
Sendio::Emails.send({ from: "bad", to: ["delivered@send.io"], subject: "x", html: "x" })
|
|
143
|
+
rescue Sendio::Error::RateLimitExceededError => e
|
|
144
|
+
sleep e.retry_after.to_i
|
|
145
|
+
rescue Sendio::Error::InvalidRequestError, Sendio::Error::NotFoundError => e
|
|
146
|
+
warn "request rejected: #{e.message}"
|
|
147
|
+
rescue Sendio::Error => e
|
|
148
|
+
warn "send.io error: #{e.message}"
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Rails and ActionMailer
|
|
153
|
+
|
|
154
|
+
Use send.io as an ActionMailer delivery method in `config/environments/production.rb`:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
config.action_mailer.delivery_method = :sendio
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Set your API key in an initializer:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# config/initializers/sendio.rb
|
|
164
|
+
Sendio.api_key = ENV["SENDIO_API_KEY"]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Then deliver as usual:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
class UserMailer < ApplicationMailer
|
|
171
|
+
default from: "Acme <onboarding@yourdomain.com>"
|
|
172
|
+
|
|
173
|
+
def welcome_email
|
|
174
|
+
@user = params[:user]
|
|
175
|
+
mail(to: ["delivered@send.io"], subject: "Welcome")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
UserMailer.with(user: user).welcome_email.deliver_now!
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Rails and Action Mailbox
|
|
183
|
+
|
|
184
|
+
The gem ships an Action Mailbox ingress so you can receive inbound email through send.io:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# config/environments/production.rb
|
|
188
|
+
config.action_mailbox.ingress = :sendio
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Create a webhook (in the dashboard or with `Sendio::Webhooks.create`) subscribed to `email.received` and pointed at:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
https://example.com/rails/action_mailbox/sendio/inbound_emails
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Store the webhook signing secret (`whsec_...`) under `action_mailbox.sendio_signing_secret` in your Rails credentials, or in the `SENDIO_INGRESS_SIGNING_SECRET` environment variable. Each verified webhook is turned back into the full email and routed like any other ingress.
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
bundle install
|
|
203
|
+
bundle exec rspec
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`examples/smoke.rb` is a live check against a real API (sends one email). It needs `SENDIO_API_KEY` and, optionally, `SENDIO_BASE_URL`.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
module Ingresses
|
|
5
|
+
module Sendio
|
|
6
|
+
# Ingests inbound emails delivered by send.io's +email.received+ webhook.
|
|
7
|
+
#
|
|
8
|
+
# send.io webhooks only carry the received email's metadata, so the full message
|
|
9
|
+
# (including any attachments) is fetched from the send.io API before being enqueued
|
|
10
|
+
# for routing. The webhook request is authenticated by verifying its Svix signature
|
|
11
|
+
# against the webhook's signing secret, which is stored in the
|
|
12
|
+
# +action_mailbox.sendio_signing_secret+ Rails credential or the
|
|
13
|
+
# +SENDIO_INGRESS_SIGNING_SECRET+ environment variable.
|
|
14
|
+
#
|
|
15
|
+
# Returns:
|
|
16
|
+
#
|
|
17
|
+
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for
|
|
18
|
+
# routing, or if the event is not an +email.received+ event and was ignored
|
|
19
|
+
# - <tt>401 Unauthorized</tt> if the request's signature could not be validated
|
|
20
|
+
# - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails
|
|
21
|
+
# from Sendio
|
|
22
|
+
# - <tt>422 Unprocessable Entity</tt> if the request's payload is malformed
|
|
23
|
+
# - <tt>500 Server Error</tt> if the webhook signing secret is missing, the send.io API
|
|
24
|
+
# could not be reached, or one of the Active Record database, the Active Storage
|
|
25
|
+
# service, or the Active Job backend is misconfigured or unavailable
|
|
26
|
+
class InboundEmailsController < ActionMailbox::BaseController
|
|
27
|
+
before_action :authenticate
|
|
28
|
+
|
|
29
|
+
def create
|
|
30
|
+
return head :no_content unless email_received_event?
|
|
31
|
+
return head :unprocessable_entity if email_id.nil?
|
|
32
|
+
|
|
33
|
+
ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email
|
|
34
|
+
head :no_content
|
|
35
|
+
rescue JSON::ParserError => e
|
|
36
|
+
logger.error e.message
|
|
37
|
+
head :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def raw_email
|
|
43
|
+
::Sendio::ActionMailbox::MessageBuilder.new(email_id).raw_email
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def email_received_event?
|
|
47
|
+
event["type"] == "email.received"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def email_id
|
|
51
|
+
event.dig("data", "email_id")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def event
|
|
55
|
+
@event ||= JSON.parse(request.raw_post)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def authenticate
|
|
59
|
+
head :unauthorized unless authenticated?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def authenticated?
|
|
63
|
+
if signing_secret.present?
|
|
64
|
+
verified_signature?
|
|
65
|
+
else
|
|
66
|
+
raise ArgumentError, <<~MESSAGE.squish
|
|
67
|
+
Missing required Sendio webhook signing secret. Set action_mailbox.sendio_signing_secret
|
|
68
|
+
in your application's encrypted credentials or provide the SENDIO_INGRESS_SIGNING_SECRET
|
|
69
|
+
environment variable.
|
|
70
|
+
MESSAGE
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def verified_signature?
|
|
75
|
+
::Sendio::Webhooks.verify(
|
|
76
|
+
payload: request.raw_post,
|
|
77
|
+
headers: {
|
|
78
|
+
svix_id: request.headers["svix-id"],
|
|
79
|
+
svix_timestamp: request.headers["svix-timestamp"],
|
|
80
|
+
svix_signature: request.headers["svix-signature"]
|
|
81
|
+
},
|
|
82
|
+
webhook_secret: signing_secret
|
|
83
|
+
)
|
|
84
|
+
rescue RuntimeError
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def signing_secret
|
|
89
|
+
Rails.application.credentials.dig(:action_mailbox, :sendio_signing_secret) ||
|
|
90
|
+
ENV["SENDIO_INGRESS_SIGNING_SECRET"]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
|
|
5
|
+
module Sendio
|
|
6
|
+
module ActionMailbox
|
|
7
|
+
# Rails engine that routes send.io's +email.received+ webhooks to the
|
|
8
|
+
# Action Mailbox ingress controller.
|
|
9
|
+
class Engine < ::Rails::Engine
|
|
10
|
+
initializer "sendio.action_mailbox.routes" do |app|
|
|
11
|
+
app.routes.append do
|
|
12
|
+
post "/rails/action_mailbox/sendio/inbound_emails",
|
|
13
|
+
to: "action_mailbox/ingresses/sendio/inbound_emails#create",
|
|
14
|
+
as: :rails_sendio_inbound_emails
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mail"
|
|
4
|
+
|
|
5
|
+
module Sendio
|
|
6
|
+
module ActionMailbox
|
|
7
|
+
# Rebuilds the full RFC 822 source of a received email so it can be ingested
|
|
8
|
+
# by Action Mailbox.
|
|
9
|
+
#
|
|
10
|
+
# send.io's +email.received+ webhook only carries metadata, so the message is
|
|
11
|
+
# fetched from the Received Emails API. When the API exposes a raw message
|
|
12
|
+
# download it is used as-is; otherwise the message is reconstructed as a
|
|
13
|
+
# Mail::Message from its parts, downloading each attachment through the
|
|
14
|
+
# attachments API.
|
|
15
|
+
class MessageBuilder
|
|
16
|
+
# Headers that describe the reconstructed MIME structure and therefore
|
|
17
|
+
# cannot be copied verbatim from the original message.
|
|
18
|
+
STRUCTURAL_HEADERS = %w[content-type content-transfer-encoding mime-version].freeze
|
|
19
|
+
|
|
20
|
+
# Envelope fields that are set from the API response when the original
|
|
21
|
+
# header of the same name is not available.
|
|
22
|
+
ENVELOPE_FIELDS = %i[from to cc bcc reply_to subject message_id].freeze
|
|
23
|
+
|
|
24
|
+
# @param email_id [String] The ID of the received email
|
|
25
|
+
def initialize(email_id)
|
|
26
|
+
@email_id = email_id
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [String] the full RFC 822 source of the received email
|
|
30
|
+
def raw_email
|
|
31
|
+
raw_download || rebuild_message.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def email
|
|
37
|
+
@email ||= Sendio::Emails::Receiving.get(@email_id, html_format: "cid")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def raw_download
|
|
41
|
+
url = fetch(email[:raw], "download_url") unless blank?(email[:raw])
|
|
42
|
+
download(url) unless blank?(url)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def rebuild_message
|
|
46
|
+
Mail.new.tap do |mail|
|
|
47
|
+
copy_headers(mail)
|
|
48
|
+
copy_envelope(mail)
|
|
49
|
+
add_bodies(mail)
|
|
50
|
+
add_attachments(mail)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def copy_headers(mail)
|
|
55
|
+
(email[:headers] || {}).each do |name, value|
|
|
56
|
+
next if STRUCTURAL_HEADERS.include?(name.to_s.downcase)
|
|
57
|
+
|
|
58
|
+
Array(value).each { |val| mail.header[name.to_s] = val }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def copy_envelope(mail)
|
|
63
|
+
ENVELOPE_FIELDS.each do |field|
|
|
64
|
+
value = email[field]
|
|
65
|
+
next if blank?(value) || mail.header[field.to_s.tr("_", "-")]
|
|
66
|
+
|
|
67
|
+
mail.public_send("#{field}=", value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
mail.date ||= email[:created_at]
|
|
71
|
+
|
|
72
|
+
expose_bcc(mail)
|
|
73
|
+
copy_received_for(mail)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Mail omits Bcc when serializing a message by default, but Action Mailbox
|
|
77
|
+
# needs it to route the email.
|
|
78
|
+
def expose_bcc(mail)
|
|
79
|
+
bcc = mail.header["bcc"]
|
|
80
|
+
bcc.include_in_headers = true if bcc.respond_to?(:include_in_headers)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Preserve the addresses this email was received for (e.g. through a
|
|
84
|
+
# forwarding rule) so Action Mailbox can route on them.
|
|
85
|
+
def copy_received_for(mail)
|
|
86
|
+
Array(email[:received_for]).each { |address| mail.header["X-Original-To"] = address }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def add_bodies(mail)
|
|
90
|
+
html = presence(email[:html])
|
|
91
|
+
text = presence(email[:text])
|
|
92
|
+
|
|
93
|
+
if html
|
|
94
|
+
mail.text_part = build_part("text/plain", text) if text
|
|
95
|
+
mail.html_part = build_part("text/html", html)
|
|
96
|
+
elsif text
|
|
97
|
+
mail.content_type "text/plain; charset=UTF-8"
|
|
98
|
+
mail.body text
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_part(mime_type, content)
|
|
103
|
+
Mail::Part.new(content_type: "#{mime_type}; charset=UTF-8", body: content)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_attachments(mail)
|
|
107
|
+
Array(email[:attachments]).each { |meta| add_attachment(mail, meta) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def add_attachment(mail, meta)
|
|
111
|
+
details = Sendio::Emails::Receiving::Attachments.get(email_id: @email_id, id: fetch(meta, "id"))
|
|
112
|
+
filename = fetch(meta, "filename") || details[:filename]
|
|
113
|
+
|
|
114
|
+
mail.attachments[filename] = {
|
|
115
|
+
mime_type: fetch(meta, "content_type") || details[:content_type],
|
|
116
|
+
content: download(details[:download_url])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
decorate_attachment(mail.attachments.last, meta)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def decorate_attachment(part, meta)
|
|
123
|
+
content_id = fetch(meta, "content_id")
|
|
124
|
+
part.content_id = normalize_content_id(content_id) unless blank?(content_id)
|
|
125
|
+
|
|
126
|
+
return unless fetch(meta, "content_disposition") == "inline"
|
|
127
|
+
|
|
128
|
+
part.content_disposition = "inline; filename=\"#{part.filename}\""
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def normalize_content_id(content_id)
|
|
132
|
+
content_id.start_with?("<") ? content_id : "<#{content_id}>"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def download(url)
|
|
136
|
+
response = HTTParty.get(url)
|
|
137
|
+
|
|
138
|
+
unless response.code == 200
|
|
139
|
+
raise Sendio::Error::ServerError.new(
|
|
140
|
+
"Failed to download received email content (HTTP #{response.code})", response.code
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
response.body
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Nested objects come back from the API with string keys, but hand-built
|
|
148
|
+
# hashes (tests, console usage) often use symbols. Accept both.
|
|
149
|
+
def fetch(hash, key)
|
|
150
|
+
hash[key] || hash[key.to_sym]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def presence(value)
|
|
154
|
+
value unless blank?(value)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def blank?(value)
|
|
158
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendio
|
|
4
|
+
# api keys api wrapper
|
|
5
|
+
module ApiKeys
|
|
6
|
+
class << self
|
|
7
|
+
# https://developer.send.io/docs/api
|
|
8
|
+
def create(params)
|
|
9
|
+
path = "api-keys"
|
|
10
|
+
Sendio::Request.new(path, params, "post").perform
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# https://developer.send.io/docs/api
|
|
14
|
+
def list(params = {})
|
|
15
|
+
path = Sendio::PaginationHelper.build_paginated_path("api-keys", params)
|
|
16
|
+
Sendio::Request.new(path, {}, "get").perform
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# https://developer.send.io/docs/api
|
|
20
|
+
def remove(api_key_id = "")
|
|
21
|
+
path = "api-keys/#{api_key_id}"
|
|
22
|
+
Sendio::Request.new(path, {}, "delete").perform
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# https://developer.send.io/docs/api
|
|
26
|
+
def update(params)
|
|
27
|
+
path = "api-keys/#{params[:id]}"
|
|
28
|
+
Sendio::Request.new(path, { name: params[:name] }, "patch").perform
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendio
|
|
4
|
+
module Automations
|
|
5
|
+
# Automation Runs api wrapper
|
|
6
|
+
module Runs
|
|
7
|
+
class << self
|
|
8
|
+
# https://developer.send.io/docs/api
|
|
9
|
+
def list(automation_id, params = {})
|
|
10
|
+
base_path = "automations/#{automation_id}/runs"
|
|
11
|
+
path = Sendio::PaginationHelper.build_paginated_path(base_path, params)
|
|
12
|
+
Sendio::Request.new(path, {}, "get").perform
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# https://developer.send.io/docs/api
|
|
16
|
+
def get(automation_id, run_id)
|
|
17
|
+
Sendio::Request.new("automations/#{automation_id}/runs/#{run_id}", {}, "get").perform
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendio
|
|
4
|
+
module Automations
|
|
5
|
+
# Automation Templates api wrapper (ready-made automations you can create from)
|
|
6
|
+
module Templates
|
|
7
|
+
class << self
|
|
8
|
+
# List the automation template catalog. Not paginated.
|
|
9
|
+
# see more: https://developer.send.io/docs/api
|
|
10
|
+
#
|
|
11
|
+
# @param params [Hash] Optional parameters
|
|
12
|
+
# @option params [String] :category Only templates in this category: "onboarding",
|
|
13
|
+
# "billing", "ecommerce", "engagement", "lead_capture" or "internal"
|
|
14
|
+
def list(params = {})
|
|
15
|
+
query_params = {}
|
|
16
|
+
query_params[:category] = params[:category] if params[:category]
|
|
17
|
+
Sendio::Request.new("automation-templates", query_params, "get").perform
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Retrieve one automation template in full (how_it_works, events with schemas,
|
|
21
|
+
# emails and params).
|
|
22
|
+
# see more: https://developer.send.io/docs/api
|
|
23
|
+
def get(slug)
|
|
24
|
+
Sendio::Request.new("automation-templates/#{ERB::Util.url_encode(slug.to_s)}", {}, "get").perform
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|