unione-rails 1.1.3
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/AGENTS.md +98 -0
- data/LICENSE +21 -0
- data/README.md +274 -0
- data/lib/unione/wrapper/delivery_method.rb +33 -0
- data/lib/unione/wrapper/email_payload.rb +91 -0
- data/lib/unione/wrapper/railtie.rb +41 -0
- data/lib/unione/wrapper/version.rb +9 -0
- data/lib/unione/wrapper/webhook_endpoint.rb +47 -0
- data/lib/unione/wrapper/webhook_signature.rb +29 -0
- data/lib/unione/wrapper.rb +70 -0
- data/lib/unione-rails.rb +3 -0
- metadata +110 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c8d33828c26cc73b7a60e6eda7fb390bbc11355ee8c59c390579eb9a304f1434
|
|
4
|
+
data.tar.gz: f26567d24240704ee28d003cfb70a1192393661a7ad4d7f0a9c0182e725d2c4c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5f18c81a7d3ce510e6e14100ae72c5886d9c69f4b4920d74523595e40d1a4638ef840cc93c2178ae8ce65bd4d7101190e5eac7e13ebbd80cbcad886204f2c729
|
|
7
|
+
data.tar.gz: da6fd0b07611a2d119bb094b18f77880a0b05291f31638aab1924c80b63484286cc56d05e3b9494c1715adfd534416b6cad32e77736dca7d35c14d2993a3f3a1
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
UniOne Ruby on Rails — Ruby on Rails integration of the UniOne Ruby SDK (`unione`) for the UniOne transactional email Web API (Web API 1.90).
|
|
4
|
+
|
|
5
|
+
## Working on this repository
|
|
6
|
+
|
|
7
|
+
- **Generated code.** The whole package is generated by the UniOne team from the UniOne Web API OpenAPI spec (Web API 1.90): sources, docs, README.md and this file. Do not hand-edit it: the next release replaces the content of this repository. Report problems and requests in the issues of this repository (https://github.com/unione-repo/unione-rails).
|
|
8
|
+
- **Build and check:**
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
gem build *.gemspec
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **Source label.** Every request carries the query parameter `platform=rails.1.1.3,ruby-sdk.1.1.3` (this package, then the SDK it is built on).
|
|
15
|
+
- **Tests.** Before a release the generator installs the package into a test app of Ruby on Rails and checks the client, the webhook endpoint and the mailer against a mock of the API: no real API key, no real sends.
|
|
16
|
+
- **Release.** The version (1.1.3 in the gemspec) and the tag `v1.1.3` come from the generator; README.md states the Web API version the release was generated from.
|
|
17
|
+
- **Secrets.** Never commit API keys; the examples read `UNIONE_API_KEY` from the environment.
|
|
18
|
+
|
|
19
|
+
## Integrating UniOne with this package
|
|
20
|
+
|
|
21
|
+
Full guide: [README.md](README.md). Ruby on Rails integration of the [UniOne Ruby SDK](https://github.com/unione-repo/unione-ruby-sdk) (`unione`): the API client in the application config (`config.unione`), a webhook endpoint with signature check, a Action Mailer transport that sends through `email/send`.
|
|
22
|
+
|
|
23
|
+
Install:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle add unione-rails
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requirements: Ruby 2.7+, Rails 6.1+.
|
|
30
|
+
|
|
31
|
+
Rules that generated code must follow:
|
|
32
|
+
|
|
33
|
+
- Server-side only. Read the API key from `UNIONE_API_KEY`; never put it in browser or mobile code, never commit it.
|
|
34
|
+
- Base URL `https://api.unione.io/en/transactional/api/v1` (an account registered on another host uses that one: `eu1.unione.io`, `us1.unione.io`); every method is POST with a JSON body; auth header `X-API-KEY`. The package does all of it.
|
|
35
|
+
- First email without DNS: `domain/get-sandbox-domain` → `confirmed-email/create` (the user clicks the link within 48 hours) → `email/send` from `noreply@<sandbox-domain>` to the confirmed address. Account API key only; up to 10 confirmed addresses, 100 emails a day; the domain expires after 31 days.
|
|
36
|
+
- Never ship a sandbox domain as the production sender (it is not in `domain/list`); read `from_email` from config. Production needs a verified domain: `domain/get-dns-records` → DNS records → `domain/validate-verification-record`, `domain/validate-dkim` → `domain/list`.
|
|
37
|
+
- Set `message.idempotence_key` on retryable sends; retry network errors, `429` and `5xx` with backoff, not other errors unchanged. Up to 500 recipients and 10 MB per request, each attachment up to 7 MB; always `body.html` or `body.plaintext`.
|
|
38
|
+
- Delivery events via webhook (`webhook/set`), not polling; the webhook endpoint of this package checks the `auth` signature of every notification.
|
|
39
|
+
- Keep the default unsubscribe and tracking behaviour; changes go through UniOne support.
|
|
40
|
+
- Do not invent methods or fields. Check the [README](README.md), the [API reference](https://docs.unione.io/en/web-api-ref) or [llms.txt](https://docs.unione.io/llms.txt).
|
|
41
|
+
|
|
42
|
+
### Configuration
|
|
43
|
+
|
|
44
|
+
The API key goes to `UNIONE_API_KEY`.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# .env / environment: UNIONE_API_KEY=YOUR_API_KEY
|
|
48
|
+
# or config/application.rb:
|
|
49
|
+
config.unione.api_key = ENV.fetch('UNIONE_API_KEY')
|
|
50
|
+
config.unione.host = 'us1.unione.io'
|
|
51
|
+
config.action_mailer.delivery_method = :unione
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Option | Environment variable | Default | Description |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `config.unione.api_key` | `UNIONE_API_KEY` | | User or project API key (required). |
|
|
57
|
+
| `config.unione.host` | `UNIONE_HOST` | `api.unione.io` | API host (datacenter: `api.unione.io`, `eu1.unione.io`, `us1.unione.io`) or a full base URL. |
|
|
58
|
+
| `config.unione.timeout` | `UNIONE_TIMEOUT` | `30` | Request timeout, seconds. |
|
|
59
|
+
| `config.unione.webhook_key` | `UNIONE_WEBHOOK_KEY` | | API key the webhooks are signed with (the key of the user/project of the webhook), if it differs from the API key. |
|
|
60
|
+
| `config.unione.webhook_path` | `UNIONE_WEBHOOK_PATH` | `/unione/webhook` | Path of the webhook endpoint. |
|
|
61
|
+
|
|
62
|
+
### Sending mail
|
|
63
|
+
|
|
64
|
+
Messages of the Action Mailer go through [email/send](https://docs.unione.io/en/web-api-ref#email-send): from, to/cc/bcc, reply-to, subject, HTML and text parts, attachments, inline images and `X-*` headers.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class OrderMailer < ApplicationMailer
|
|
68
|
+
def shipped(order)
|
|
69
|
+
headers['X-Order-Id'] = order.id.to_s
|
|
70
|
+
attachments['invoice.pdf'] = order.invoice_pdf
|
|
71
|
+
mail(to: order.email, cc: order.manager_email, subject: 'Order shipped')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
message = OrderMailer.shipped(order).deliver_now
|
|
76
|
+
message.delivery_method.response['job_id']
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Webhooks
|
|
80
|
+
|
|
81
|
+
The endpoint answers `GET` with 200 OK (the URL check of [webhook/set](https://docs.unione.io/en/web-api-ref#webhook-set)) and accepts the `POST` notifications: it checks the `auth` field (MD5 of the request body in which the `auth` value is replaced by the API key), answers 401 to a wrong signature, and passes the notification and each of its events to the application.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# config/initializers/unione.rb
|
|
85
|
+
ActiveSupport::Notifications.subscribe('event.unione') do |*, payload|
|
|
86
|
+
event = payload[:event]
|
|
87
|
+
data = event.event_data
|
|
88
|
+
Rails.logger.info("UniOne #{event.event_name}: #{data&.email} #{data&.status}")
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Register the URL of the endpoint with `webhook/set`, e.g. `https://example.com/unione/webhook`.
|
|
93
|
+
|
|
94
|
+
### Reference
|
|
95
|
+
|
|
96
|
+
- [UniOne Ruby SDK](https://github.com/unione-repo/unione-ruby-sdk): every API method, models, errors ([AGENTS.md](https://github.com/unione-repo/unione-ruby-sdk/blob/HEAD/AGENTS.md))
|
|
97
|
+
- [API reference](https://docs.unione.io/en/web-api-ref)
|
|
98
|
+
- [Full API reference for LLMs](https://docs.unione.io/llms-full.txt)
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 UniOne
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# UniOne Ruby on Rails 1.1.3
|
|
2
|
+
|
|
3
|
+
Web API 1.90
|
|
4
|
+
|
|
5
|
+
Quickstart guide: [Send emails with Ruby](https://docs.unione.io/en/send-with-ruby) · Docs for LLMs: [llms.txt](https://docs.unione.io/llms.txt) · [API reference](https://docs.unione.io/en/web-api-ref)
|
|
6
|
+
|
|
7
|
+
Ruby on Rails integration of the [UniOne Ruby SDK](https://github.com/unione-repo/unione-ruby-sdk) (`unione`): the API client in the application config (`config.unione`), a webhook endpoint with signature check, a Action Mailer transport that sends through `email/send`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle add unione-rails
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requirements: Ruby 2.7+, Rails 6.1+.
|
|
16
|
+
|
|
17
|
+
## Get an API key
|
|
18
|
+
|
|
19
|
+
1. Sign up at [cp.unione.io](https://cp.unione.io/en/user/registration/).
|
|
20
|
+
2. Copy your API key from [Settings → Security → API](https://cp.unione.io/en/settings/security/api).
|
|
21
|
+
3. Put it in an environment variable:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export UNIONE_API_KEY="your-api-key"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The package reads it from `UNIONE_API_KEY` (see the configuration below).
|
|
28
|
+
|
|
29
|
+
> **Server-side only.** Use this package in backend code: API routes, server functions, workers.
|
|
30
|
+
> Never ship the API key to a browser or a mobile app, and never commit it.
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Settings are read from the environment by default:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# .env / environment: UNIONE_API_KEY=YOUR_API_KEY
|
|
38
|
+
# or config/application.rb:
|
|
39
|
+
config.unione.api_key = ENV.fetch('UNIONE_API_KEY')
|
|
40
|
+
config.unione.host = 'us1.unione.io'
|
|
41
|
+
config.action_mailer.delivery_method = :unione
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Option | Environment variable | Default | Description |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| `config.unione.api_key` | `UNIONE_API_KEY` | | User or project API key (required). |
|
|
47
|
+
| `config.unione.host` | `UNIONE_HOST` | `api.unione.io` | API host (datacenter: `api.unione.io`, `eu1.unione.io`, `us1.unione.io`) or a full base URL. |
|
|
48
|
+
| `config.unione.timeout` | `UNIONE_TIMEOUT` | `30` | Request timeout, seconds. |
|
|
49
|
+
| `config.unione.webhook_key` | `UNIONE_WEBHOOK_KEY` | | API key the webhooks are signed with (the key of the user/project of the webhook), if it differs from the API key. |
|
|
50
|
+
| `config.unione.webhook_path` | `UNIONE_WEBHOOK_PATH` | `/unione/webhook` | Path of the webhook endpoint. |
|
|
51
|
+
|
|
52
|
+
The API key can also be stored in the credentials (`unione: { api_key: ... }`). The webhook route `unione_webhook` is a Rack endpoint (no CSRF, no session); `config.unione.webhook_enabled = false` turns it off.
|
|
53
|
+
|
|
54
|
+
## Send your first email — no DNS setup
|
|
55
|
+
|
|
56
|
+
Every account can send test email right away from a **sandbox domain**: pre-verified, DKIM and SPF already set.
|
|
57
|
+
It delivers only to addresses you confirm.
|
|
58
|
+
|
|
59
|
+
The examples use the API client of the package (see the configuration above).
|
|
60
|
+
|
|
61
|
+
**1. Get your sandbox domain** (`domain/get-sandbox-domain`). The call creates the domain on first use and returns the same one afterwards.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
response = UniOne::Wrapper.client.domain.get_sandbox_domain
|
|
65
|
+
puts response.domain
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**2. Confirm your inbox** (`confirmed-email/create`). UniOne sends a confirmation email; click the link in it within 48 hours.
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
ConfirmedEmail = UniOne::Api::ConfirmedEmail::Model
|
|
72
|
+
|
|
73
|
+
response = UniOne::Wrapper.client.confirmed_email.create(ConfirmedEmail::ConfirmedEmailRequest.new(
|
|
74
|
+
email: 'you@example.com'
|
|
75
|
+
))
|
|
76
|
+
puts response.status
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Check the status with `confirmed-email/list` — your address should show `"status": "confirmed"`.
|
|
80
|
+
|
|
81
|
+
**3. Send the email** (`email/send`) from any address on the sandbox domain to the confirmed address.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Common = UniOne::Common::Model
|
|
85
|
+
Email = UniOne::Api::Email::Model
|
|
86
|
+
|
|
87
|
+
response = UniOne::Wrapper.client.email.send_email(Email::EmailSendRequest.new(
|
|
88
|
+
message: Email::MessageObject.new(
|
|
89
|
+
recipients: [
|
|
90
|
+
Email::MessageObjectRecipientsItem.new(
|
|
91
|
+
email: 'you@example.com'
|
|
92
|
+
)
|
|
93
|
+
],
|
|
94
|
+
body: Common::BodyObject.new(
|
|
95
|
+
html: '<p>It works!</p>'
|
|
96
|
+
),
|
|
97
|
+
subject: 'Hello from UniOne',
|
|
98
|
+
from_email: 'noreply@<sandbox-domain>'
|
|
99
|
+
)
|
|
100
|
+
))
|
|
101
|
+
puts response.job_id
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### What you get back
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"status": "success",
|
|
109
|
+
"job_id": "1ZymBc-00041N-9X",
|
|
110
|
+
"emails": [
|
|
111
|
+
"you@example.com"
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
On error the API returns `status: "error"`, a `message` and a numeric `code`:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"status": "error",
|
|
121
|
+
"code": 102,
|
|
122
|
+
"message": "Error ID:83274F98-C7D5-11F1-B221-6214FF8707B1. Can not decode key wrongkey"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Same flow with curl
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
BASE=https://api.unione.io/en/transactional/api/v1
|
|
130
|
+
|
|
131
|
+
curl -s -X POST $BASE/domain/get-sandbox-domain.json \
|
|
132
|
+
-H "Content-Type: application/json" -H "X-API-KEY: $UNIONE_API_KEY" -d '{}'
|
|
133
|
+
|
|
134
|
+
curl -s -X POST $BASE/confirmed-email/create.json \
|
|
135
|
+
-H "Content-Type: application/json" -H "X-API-KEY: $UNIONE_API_KEY" \
|
|
136
|
+
-d '{"email": "you@example.com"}'
|
|
137
|
+
|
|
138
|
+
curl -s -X POST $BASE/email/send.json \
|
|
139
|
+
-H "Content-Type: application/json" -H "X-API-KEY: $UNIONE_API_KEY" \
|
|
140
|
+
-d '{
|
|
141
|
+
"message": {
|
|
142
|
+
"recipients": [
|
|
143
|
+
{
|
|
144
|
+
"email": "you@example.com"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"from_email": "noreply@<sandbox-domain>",
|
|
148
|
+
"subject": "Hello from UniOne",
|
|
149
|
+
"body": {
|
|
150
|
+
"html": "<p>It works!</p>"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Sending mail
|
|
157
|
+
|
|
158
|
+
Messages of the Action Mailer go through [email/send](https://docs.unione.io/en/web-api-ref#email-send): from, to/cc/bcc, reply-to, subject, HTML and text parts, attachments, inline images and `X-*` headers.
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
class OrderMailer < ApplicationMailer
|
|
162
|
+
def shipped(order)
|
|
163
|
+
headers['X-Order-Id'] = order.id.to_s
|
|
164
|
+
attachments['invoice.pdf'] = order.invoice_pdf
|
|
165
|
+
mail(to: order.email, cc: order.manager_email, subject: 'Order shipped')
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
message = OrderMailer.shipped(order).deliver_now
|
|
170
|
+
message.delivery_method.response['job_id']
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Webhooks
|
|
174
|
+
|
|
175
|
+
The endpoint answers `GET` with 200 OK (the URL check of [webhook/set](https://docs.unione.io/en/web-api-ref#webhook-set)) and accepts the `POST` notifications: it checks the `auth` field (MD5 of the request body in which the `auth` value is replaced by the API key), answers 401 to a wrong signature, and passes the notification and each of its events to the application.
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# config/initializers/unione.rb
|
|
179
|
+
ActiveSupport::Notifications.subscribe('event.unione') do |*, payload|
|
|
180
|
+
event = payload[:event]
|
|
181
|
+
data = event.event_data
|
|
182
|
+
Rails.logger.info("UniOne #{event.event_name}: #{data&.email} #{data&.status}")
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Register the URL of the endpoint with `webhook/set`, e.g. `https://example.com/unione/webhook`.
|
|
187
|
+
|
|
188
|
+
## API client
|
|
189
|
+
|
|
190
|
+
Every section of the API is available on the client, see the [SDK docs](https://github.com/unione-repo/unione-ruby-sdk).
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
client = UniOne::Wrapper.client
|
|
194
|
+
info = client.system.info
|
|
195
|
+
puts info.accounting&.emails_sent
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Sending through the API
|
|
199
|
+
|
|
200
|
+
Besides the Action Mailer, mail can be sent through the API client directly. The client calls [email/send](https://docs.unione.io/en/web-api-ref#email-send) with all of its options: templates, substitutions, metadata, tracking. Fields of the request and other examples: [Ruby SDK docs](https://github.com/unione-repo/unione-ruby-sdk/blob/HEAD/docs/Email.md#send_email).
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
Common = UniOne::Common::Model
|
|
204
|
+
Email = UniOne::Api::Email::Model
|
|
205
|
+
|
|
206
|
+
response = UniOne::Wrapper.client.email.send_email(Email::EmailSendRequest.new(
|
|
207
|
+
message: Email::MessageObject.new(
|
|
208
|
+
recipients: [
|
|
209
|
+
Email::MessageObjectRecipientsItem.new(
|
|
210
|
+
email: 'user@example.com',
|
|
211
|
+
substitutions: {
|
|
212
|
+
'CustomerId' => '12452',
|
|
213
|
+
'to_name' => 'John Smith',
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
],
|
|
217
|
+
body: Common::BodyObject.new(
|
|
218
|
+
html: '<b>Hello, {{to_name}}</b>',
|
|
219
|
+
plaintext: 'Hello, {{to_name}}'
|
|
220
|
+
),
|
|
221
|
+
subject: 'UniOne test email ',
|
|
222
|
+
from_email: 'email@example.com',
|
|
223
|
+
from_name: 'John Smith'
|
|
224
|
+
)
|
|
225
|
+
))
|
|
226
|
+
puts response.job_id
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Sandbox limits
|
|
230
|
+
|
|
231
|
+
- Sends only to confirmed addresses: up to 10 per account.
|
|
232
|
+
- Up to 100 emails a day.
|
|
233
|
+
- The sandbox domain expires after 31 days; call `domain/get-sandbox-domain` again to get a new one.
|
|
234
|
+
- Works only with the account API key, not a project key.
|
|
235
|
+
- Not listed in `domain/list`; the other `domain/*` methods do not apply to it.
|
|
236
|
+
|
|
237
|
+
The sandbox is for development and testing. Do not use a sandbox domain as the sender in production.
|
|
238
|
+
|
|
239
|
+
## Before production: verify your domain
|
|
240
|
+
|
|
241
|
+
1. `domain/get-dns-records` — returns the values for your DNS records.
|
|
242
|
+
2. Add these records at your DNS provider (the domain page of the control panel shows them too):
|
|
243
|
+
|
|
244
|
+
| Type | Host | Value |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| TXT | `@` | the `verification-record` value (`unione-validate-hash=…`) |
|
|
247
|
+
| TXT | `<selector>._domainkey` (the selector is on the domain page of the control panel, e.g. `us`) | `v=DKIM1; k=rsa; p=<dkim>` |
|
|
248
|
+
| TXT | `@` | `v=spf1 include:spf.unione.io ~all` |
|
|
249
|
+
| CNAME | `_dmarc` | the `dmarc` value |
|
|
250
|
+
|
|
251
|
+
3. `domain/validate-verification-record`, then `domain/validate-dkim`.
|
|
252
|
+
4. `domain/list` — wait until the verification record is confirmed and DKIM is active.
|
|
253
|
+
5. Read `from_email` from your config, so switching from the sandbox needs no code change.
|
|
254
|
+
|
|
255
|
+
## Sending reliably
|
|
256
|
+
|
|
257
|
+
- **Retries:** set `message.idempotence_key` on sends that may be retried, so a repeated request does not send twice.
|
|
258
|
+
- **Limits:** up to 500 recipients and 10 MB per request; each attachment up to 7 MB.
|
|
259
|
+
- **Body:** always include `body.html` or `body.plaintext`.
|
|
260
|
+
- **Delivery status:** register a webhook (`webhook/set`) with a fast, idempotent HTTPS handler instead of polling.
|
|
261
|
+
- **Defaults:** keep the default unsubscribe footer and tracking; changing them requires UniOne support.
|
|
262
|
+
- **Errors:** retry `429` and `5xx` with backoff; do not retry other errors unchanged.
|
|
263
|
+
|
|
264
|
+
## Links
|
|
265
|
+
|
|
266
|
+
- [Send emails with Ruby](https://docs.unione.io/en/send-with-ruby) and other [integration guides](https://docs.unione.io/en/integrations/)
|
|
267
|
+
- [Web API reference](https://docs.unione.io/en/web-api-ref)
|
|
268
|
+
- [llms.txt](https://docs.unione.io/llms.txt) for AI coding agents
|
|
269
|
+
- [UniOne MCP server and agent setup](https://unione.io/en/agents)
|
|
270
|
+
- [Support](https://cp.unione.io/en/support)
|
|
271
|
+
|
|
272
|
+
## For package contributors
|
|
273
|
+
|
|
274
|
+
See [AGENTS.md](AGENTS.md): how this package is generated from the OpenAPI spec, built, tested and released.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UniOne
|
|
4
|
+
module Wrapper
|
|
5
|
+
# Action Mailer delivery method :unione, sends through email/send:
|
|
6
|
+
#
|
|
7
|
+
# config.action_mailer.delivery_method = :unione
|
|
8
|
+
# config.action_mailer.unione_settings = { api_key: '...' } # optional, default: config.unione
|
|
9
|
+
#
|
|
10
|
+
# The API response ("job_id", "emails", "failed_emails") is in mail.delivery_method.response.
|
|
11
|
+
class DeliveryMethod
|
|
12
|
+
attr_accessor :settings
|
|
13
|
+
attr_reader :response
|
|
14
|
+
|
|
15
|
+
def initialize(settings = {})
|
|
16
|
+
@settings = settings || {}
|
|
17
|
+
@response = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def deliver!(mail)
|
|
21
|
+
@response = client.api_client.call('/email/send.json', { 'message' => EmailPayload.from_mail(mail) })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def client
|
|
27
|
+
return ::UniOne::Wrapper.client if @settings[:api_key].nil?
|
|
28
|
+
|
|
29
|
+
::UniOne::Wrapper.build_client(@settings)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module UniOne
|
|
6
|
+
module Wrapper
|
|
7
|
+
# Converts a Mail::Message to the "message" of email/send.
|
|
8
|
+
#
|
|
9
|
+
# - recipients: to, cc and bcc, the display name goes to the "to_name" substitution;
|
|
10
|
+
# - with cc the "To" and "CC" headers are set, so every recipient sees the same headers;
|
|
11
|
+
# - inline attachments become inline_attachments named by their Content-ID (cid:...);
|
|
12
|
+
# - X-* and List-* / In-Reply-To / References headers are passed as is.
|
|
13
|
+
module EmailPayload
|
|
14
|
+
PASSED_HEADERS = %w[list-unsubscribe list-subscribe list-help list-owner list-archive in-reply-to references].freeze
|
|
15
|
+
|
|
16
|
+
def self.from_mail(mail)
|
|
17
|
+
from = addresses(mail, :from).first
|
|
18
|
+
raise ArgumentError, 'The message has no sender (From)' if from.nil?
|
|
19
|
+
|
|
20
|
+
message = {
|
|
21
|
+
'recipients' => recipients(mail),
|
|
22
|
+
'subject' => mail.subject.to_s,
|
|
23
|
+
'from_email' => from.address
|
|
24
|
+
}
|
|
25
|
+
message['from_name'] = from.display_name if present?(from.display_name)
|
|
26
|
+
reply_to = addresses(mail, :reply_to).first
|
|
27
|
+
if reply_to
|
|
28
|
+
message['reply_to'] = reply_to.address
|
|
29
|
+
message['reply_to_name'] = reply_to.display_name if present?(reply_to.display_name)
|
|
30
|
+
end
|
|
31
|
+
body = body_parts(mail)
|
|
32
|
+
message['body'] = body unless body.empty?
|
|
33
|
+
headers = headers(mail)
|
|
34
|
+
message['headers'] = headers unless headers.empty?
|
|
35
|
+
mail.attachments.each do |part|
|
|
36
|
+
inline = part.inline? && present?(part.cid)
|
|
37
|
+
item = { 'type' => part.mime_type.to_s, 'name' => inline ? part.cid : part.filename.to_s,
|
|
38
|
+
'content' => Base64.strict_encode64(part.decoded) }
|
|
39
|
+
(message[inline ? 'inline_attachments' : 'attachments'] ||= []) << item
|
|
40
|
+
end
|
|
41
|
+
message
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.recipients(mail)
|
|
45
|
+
%i[to cc bcc].flat_map { |field| addresses(mail, field) }.map do |address|
|
|
46
|
+
recipient = { 'email' => address.address }
|
|
47
|
+
recipient['substitutions'] = { 'to_name' => address.display_name } if present?(address.display_name)
|
|
48
|
+
recipient
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.body_parts(mail)
|
|
53
|
+
html = mail.html_part
|
|
54
|
+
text = mail.text_part
|
|
55
|
+
if html.nil? && text.nil? && !mail.multipart?
|
|
56
|
+
key = mail.mime_type.to_s == 'text/html' ? 'html' : 'plaintext'
|
|
57
|
+
return { key => mail.body.decoded }
|
|
58
|
+
end
|
|
59
|
+
{ 'html' => html&.decoded, 'plaintext' => text&.decoded }.compact
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.headers(mail)
|
|
63
|
+
headers = {}
|
|
64
|
+
cc = addresses(mail, :cc)
|
|
65
|
+
unless cc.empty?
|
|
66
|
+
headers['To'] = addresses(mail, :to).map(&:to_s).join(', ')
|
|
67
|
+
headers['CC'] = cc.map(&:to_s).join(', ')
|
|
68
|
+
end
|
|
69
|
+
mail.header.fields.each do |field|
|
|
70
|
+
name = field.name.to_s
|
|
71
|
+
next unless name.downcase.start_with?('x-') || PASSED_HEADERS.include?(name.downcase)
|
|
72
|
+
|
|
73
|
+
headers[name] = field.value.to_s
|
|
74
|
+
end
|
|
75
|
+
headers
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.addresses(mail, field)
|
|
79
|
+
header = mail[field]
|
|
80
|
+
return [] if header.nil?
|
|
81
|
+
|
|
82
|
+
list = header.respond_to?(:address_list) ? header.address_list : nil
|
|
83
|
+
list ? list.addresses : Array(header.addrs)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.present?(value)
|
|
87
|
+
!value.nil? && !value.to_s.empty?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module UniOne
|
|
6
|
+
module Wrapper
|
|
7
|
+
# config.unione settings, the :unione delivery method of Action Mailer and the webhook route.
|
|
8
|
+
class Railtie < ::Rails::Railtie
|
|
9
|
+
config.unione = ::UniOne::Wrapper.default_configuration
|
|
10
|
+
|
|
11
|
+
initializer 'unione.configure' do |app|
|
|
12
|
+
options = app.config.unione
|
|
13
|
+
if options.api_key.nil? || options.api_key.to_s.empty?
|
|
14
|
+
credentials = begin
|
|
15
|
+
app.credentials.unione
|
|
16
|
+
rescue StandardError
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
options.api_key = credentials[:api_key] if credentials.respond_to?(:[])
|
|
20
|
+
end
|
|
21
|
+
::UniOne::Wrapper.configuration = options
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
initializer 'unione.action_mailer' do
|
|
25
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
26
|
+
add_delivery_method :unione, ::UniOne::Wrapper::DeliveryMethod
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
initializer 'unione.webhook_route' do |app|
|
|
31
|
+
options = app.config.unione
|
|
32
|
+
if options.webhook_enabled != false
|
|
33
|
+
app.routes.prepend do
|
|
34
|
+
match options.webhook_path, to: ::UniOne::Wrapper::WebhookEndpoint.new, via: %i[get head post],
|
|
35
|
+
as: 'unione_webhook'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'active_support/notifications'
|
|
5
|
+
|
|
6
|
+
module UniOne
|
|
7
|
+
module Wrapper
|
|
8
|
+
# Rack endpoint of the webhooks (no CSRF, no session): GET/HEAD -> 200 (URL check of webhook/set),
|
|
9
|
+
# POST -> signature check (401 when wrong), then ActiveSupport::Notifications:
|
|
10
|
+
# "webhook.unione" - the whole notification: payload[:callback] (WebhookCallback), payload[:data] (Hash)
|
|
11
|
+
# "event.unione" - every event: payload[:event], payload[:user] (user/project of the event)
|
|
12
|
+
# Subscribers must be fast: the API waits for 200 OK for 3 seconds only.
|
|
13
|
+
class WebhookEndpoint
|
|
14
|
+
HEADERS = { 'content-type' => 'text/plain' }.freeze
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
return reply(200, 'OK') unless env['REQUEST_METHOD'] == 'POST'
|
|
18
|
+
|
|
19
|
+
input = env['rack.input']
|
|
20
|
+
body = input ? input.read.to_s : ''
|
|
21
|
+
input&.rewind if input.respond_to?(:rewind)
|
|
22
|
+
return reply(401, 'Invalid signature') unless WebhookSignature.verify(body, ::UniOne::Wrapper.webhook_key)
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
callback = ::UniOne::Api::Webhook::WebhookApi.parse_callback(body)
|
|
26
|
+
data = JSON.parse(body)
|
|
27
|
+
rescue JSON::ParserError, TypeError, NoMethodError
|
|
28
|
+
return reply(400, 'Invalid body')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
ActiveSupport::Notifications.instrument('webhook.unione', callback: callback, data: data)
|
|
32
|
+
(callback.events_by_user || []).each do |user|
|
|
33
|
+
(user.events || []).each do |event|
|
|
34
|
+
ActiveSupport::Notifications.instrument('event.unione', event: event, user: user)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
reply(200, 'OK')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def reply(status, text)
|
|
43
|
+
[status, HEADERS.dup, [text]]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest/md5'
|
|
4
|
+
require 'active_support/security_utils'
|
|
5
|
+
|
|
6
|
+
module UniOne
|
|
7
|
+
module Wrapper
|
|
8
|
+
# Signature of the webhook notifications: the "auth" field is the MD5 of the request body in which the "auth"
|
|
9
|
+
# value is replaced by the API key of the user/project of the webhook.
|
|
10
|
+
module WebhookSignature
|
|
11
|
+
PLACEHOLDER = '0' * 32
|
|
12
|
+
|
|
13
|
+
def self.verify(body, api_key)
|
|
14
|
+
return false if api_key.nil? || api_key.empty?
|
|
15
|
+
|
|
16
|
+
match = body.match(/"auth"\s*:\s*"(\h{32})"/)
|
|
17
|
+
return false if match.nil?
|
|
18
|
+
|
|
19
|
+
expected = Digest::MD5.hexdigest(body.gsub(match[1], api_key))
|
|
20
|
+
ActiveSupport::SecurityUtils.secure_compare(match[1].downcase, expected)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Signs a body that has "auth" set to the placeholder, e.g. for tests.
|
|
24
|
+
def self.sign(body, api_key, placeholder = PLACEHOLDER)
|
|
25
|
+
body.gsub(placeholder, Digest::MD5.hexdigest(body.gsub(placeholder, api_key)))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'unione'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
require 'active_support/ordered_options'
|
|
6
|
+
require_relative 'wrapper/version'
|
|
7
|
+
require_relative 'wrapper/webhook_signature'
|
|
8
|
+
require_relative 'wrapper/email_payload'
|
|
9
|
+
require_relative 'wrapper/delivery_method'
|
|
10
|
+
require_relative 'wrapper/webhook_endpoint'
|
|
11
|
+
|
|
12
|
+
module UniOne
|
|
13
|
+
# Rails integration: UniOne::Wrapper.client is the UniOne API client configured by config.unione.
|
|
14
|
+
module Wrapper
|
|
15
|
+
class << self
|
|
16
|
+
# Settings: api_key, host, timeout, webhook_key, webhook_path, webhook_enabled (config.unione in Rails).
|
|
17
|
+
def configuration
|
|
18
|
+
@configuration ||= default_configuration
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configuration=(options)
|
|
22
|
+
@configuration = options
|
|
23
|
+
@client = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def configure
|
|
27
|
+
yield configuration
|
|
28
|
+
@client = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [UniOne::UniOneClient] memoized client of the configuration
|
|
32
|
+
def client
|
|
33
|
+
@client ||= build_client(configuration)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A client for other settings: { api_key:, host:, timeout: } (missing ones come from the configuration).
|
|
37
|
+
def build_client(options)
|
|
38
|
+
api_key = present(options[:api_key])
|
|
39
|
+
raise ArgumentError, 'UniOne: set config.unione.api_key (UNIONE_API_KEY)' if api_key.nil?
|
|
40
|
+
|
|
41
|
+
timeout = present(options[:timeout]) || configuration[:timeout] || 30
|
|
42
|
+
::UniOne::UniOneClient.new(api_key, present(options[:host]), timeout: timeout.to_i, platform: PLATFORM)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# API key the webhook notifications are signed with.
|
|
46
|
+
def webhook_key
|
|
47
|
+
present(configuration[:webhook_key]) || present(configuration[:api_key]).to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def default_configuration
|
|
51
|
+
options = ActiveSupport::OrderedOptions.new
|
|
52
|
+
options.api_key = ENV['UNIONE_API_KEY']
|
|
53
|
+
options.host = ENV['UNIONE_HOST']
|
|
54
|
+
options.timeout = (ENV['UNIONE_TIMEOUT'] || 30).to_i
|
|
55
|
+
options.webhook_key = ENV['UNIONE_WEBHOOK_KEY']
|
|
56
|
+
options.webhook_path = ENV['UNIONE_WEBHOOK_PATH'] || '/unione/webhook'
|
|
57
|
+
options.webhook_enabled = ENV['UNIONE_WEBHOOK_ENABLED'] != 'false'
|
|
58
|
+
options
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def present(value)
|
|
64
|
+
value.nil? || value.to_s.empty? ? nil : value
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
require_relative 'wrapper/railtie' if defined?(::Rails::Railtie)
|
data/lib/unione-rails.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: unione-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- UniOne
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: unione
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 1.1.3
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 1.1.3
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: actionmailer
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '6.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '6.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: actionpack
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '6.1'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '6.1'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: railties
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '6.1'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '6.1'
|
|
69
|
+
description:
|
|
70
|
+
email:
|
|
71
|
+
executables: []
|
|
72
|
+
extensions: []
|
|
73
|
+
extra_rdoc_files: []
|
|
74
|
+
files:
|
|
75
|
+
- AGENTS.md
|
|
76
|
+
- LICENSE
|
|
77
|
+
- README.md
|
|
78
|
+
- lib/unione-rails.rb
|
|
79
|
+
- lib/unione/wrapper.rb
|
|
80
|
+
- lib/unione/wrapper/delivery_method.rb
|
|
81
|
+
- lib/unione/wrapper/email_payload.rb
|
|
82
|
+
- lib/unione/wrapper/railtie.rb
|
|
83
|
+
- lib/unione/wrapper/version.rb
|
|
84
|
+
- lib/unione/wrapper/webhook_endpoint.rb
|
|
85
|
+
- lib/unione/wrapper/webhook_signature.rb
|
|
86
|
+
homepage: https://github.com/unione-repo/unione-rails
|
|
87
|
+
licenses:
|
|
88
|
+
- MIT
|
|
89
|
+
metadata:
|
|
90
|
+
documentation_uri: https://docs.unione.io/en/web-api-ref
|
|
91
|
+
post_install_message:
|
|
92
|
+
rdoc_options: []
|
|
93
|
+
require_paths:
|
|
94
|
+
- lib
|
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '2.7'
|
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '0'
|
|
105
|
+
requirements: []
|
|
106
|
+
rubygems_version: 3.5.22
|
|
107
|
+
signing_key:
|
|
108
|
+
specification_version: 4
|
|
109
|
+
summary: 'UniOne Web API for Rails: API client, Action Mailer delivery method, webhooks'
|
|
110
|
+
test_files: []
|