notisend 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 +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +298 -0
- data/lib/notisend/client.rb +128 -0
- data/lib/notisend/collection.rb +43 -0
- data/lib/notisend/configuration.rb +20 -0
- data/lib/notisend/errors.rb +87 -0
- data/lib/notisend/resources/balance.rb +12 -0
- data/lib/notisend/resources/base.rb +30 -0
- data/lib/notisend/resources/campaigns.rb +31 -0
- data/lib/notisend/resources/lists.rb +28 -0
- data/lib/notisend/resources/messages.rb +19 -0
- data/lib/notisend/resources/organizations.rb +37 -0
- data/lib/notisend/resources/parameters.rb +24 -0
- data/lib/notisend/resources/recipients.rb +40 -0
- data/lib/notisend/resources/segments.rb +12 -0
- data/lib/notisend/resources/templates.rb +30 -0
- data/lib/notisend/resources/webhooks.rb +37 -0
- data/lib/notisend/version.rb +5 -0
- data/lib/notisend.rb +48 -0
- metadata +112 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6089d5fa5baa971c293eb3aff9654bd97b0ddb8745c89e847025e7cdb0d9c067
|
|
4
|
+
data.tar.gz: 604e5b27507d629e5f1b99bcdb00a8f7186822b3e03e4b961080c7c82dd86f9c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bb95bf6b3ce3406387c2e235ffa03da81e4e6a20b9df4bac9813d039cf113b491a7b99563137e5a58a23f723411c708563b6d50c315291c87886cbf773094c17
|
|
7
|
+
data.tar.gz: 225db3327f3c7295f35d20af6dd5d493f9755d8c724bf11a3c6c9680b0851916c99361a88910fdc84881eccb7b58daa3657cfdd220485b06b7609733a90f48d9
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-08-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Full coverage of the Notisend email API: balance, messages, templates, lists,
|
|
15
|
+
parameters, recipients (incl. bulk import and search), segments, organizations,
|
|
16
|
+
campaigns, webhooks
|
|
17
|
+
- Instance client with per-instance options and a global default client
|
|
18
|
+
(`Notisend.configure` / `Notisend.client`)
|
|
19
|
+
- Pagination via `Notisend::Collection` with `next_page` and `auto_paging_each`
|
|
20
|
+
- Typed error hierarchy with `retry_after` on 429 responses
|
|
21
|
+
- Multipart attachments support for messages, template messages and campaigns
|
|
22
|
+
- Custom SSL support: `verify_ssl`, `ca_bundle_file` (+ ENV overrides)
|
|
23
|
+
- Reserve base URL constant (`Notisend::RESERVE_BASE_URL`) for RKN-restricted IPs
|
|
24
|
+
- GET-only automatic retries on connection failures; POST is never retried
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aleksandr Dryzhuk
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Notisend
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/notisend)
|
|
4
|
+
|
|
5
|
+
Full-coverage Ruby client for the [Notisend email API](https://notisend.ru/dev/email/api/):
|
|
6
|
+
single messages, templates, recipient lists, parameters, recipients (including bulk import
|
|
7
|
+
and search), segments, organizations, campaigns, webhooks and balance.
|
|
8
|
+
|
|
9
|
+
Built on Faraday 2.x. Ruby 3.4+ (developed against Ruby 4.0).
|
|
10
|
+
|
|
11
|
+
Based on stale [notisend-ruby](https://github.com/evserykh/notiesend-ruby) repo.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle add notisend
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or in your Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'notisend'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Configure once globally (e.g. in a Rails initializer):
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
Notisend.configure do |config|
|
|
31
|
+
config.api_token = 'your-api-token' # default: ENV['NOTISEND_API_TOKEN']
|
|
32
|
+
# config.base_url = Notisend::RESERVE_BASE_URL # alternate URL, see below
|
|
33
|
+
# config.open_timeout = 10 # seconds
|
|
34
|
+
# config.timeout = 30 # seconds
|
|
35
|
+
# config.verify_ssl = true # default: true
|
|
36
|
+
# config.ca_bundle_file = '/path/to/ca.pem' # custom CA bundle
|
|
37
|
+
# config.logger = Logger.new($stdout) # logs method/path/status, never the token
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Notisend.client.balance.get
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or build explicit client instances (each option falls back to the global config):
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
client = Notisend::Client.new(api_token: 'other-token')
|
|
47
|
+
other = Notisend::Client.new(api_token: 'second-account', base_url: Notisend::RESERVE_BASE_URL)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Environment variables
|
|
51
|
+
|
|
52
|
+
| Variable | Meaning |
|
|
53
|
+
| ------------------------- | ------------------------------------------------------- |
|
|
54
|
+
| `NOTISEND_API_TOKEN` | API token (used when `api_token` is not set explicitly) |
|
|
55
|
+
| `NOTISEND_VERIFY_SSL` | Set to `'false'` to disable SSL verification |
|
|
56
|
+
| `NOTISEND_CA_BUNDLE_FILE` | Path to a custom CA bundle file |
|
|
57
|
+
|
|
58
|
+
### Alternate base URL
|
|
59
|
+
|
|
60
|
+
If your IP is blocked or restricted (RKN), use the reserve endpoint:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
Notisend.configure { |c| c.base_url = Notisend::RESERVE_BASE_URL } # https://api-reserve.msndr.net/v1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Custom SSL certificates
|
|
67
|
+
|
|
68
|
+
For environments with a custom certificate chain (e.g. Russian MinTsifra CA or a corporate
|
|
69
|
+
proxy), point the client at your CA bundle:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Notisend.configure do |config|
|
|
73
|
+
config.ca_bundle_file = '/usr/local/share/ca-certificates/russian_trusted_ca.pem'
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
SSL verification stays on by default; `Faraday::SSLError` is raised as `Notisend::SSLError`.
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
All methods return parsed JSON as plain hashes with string keys. Paginated endpoints
|
|
82
|
+
return a `Notisend::Collection` (see Pagination below).
|
|
83
|
+
|
|
84
|
+
### Balance
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
client.balance.get # => { 'tariff' => {...}, 'balance' => 12965.96 }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Messages
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
message = client.messages.deliver(
|
|
94
|
+
from_email: 'alice@example.org',
|
|
95
|
+
to: 'bob@example.org',
|
|
96
|
+
subject: 'Hello',
|
|
97
|
+
text: 'Hello, Bob!',
|
|
98
|
+
html: '<h1>Hello, Bob!</h1>', # text and/or html is required
|
|
99
|
+
from_name: 'Alice', # optional
|
|
100
|
+
payment: 'credit', # optional: subscriber_priority (default),
|
|
101
|
+
# credit_priority, subscriber, credit
|
|
102
|
+
smtp_headers: { 'Client-Id' => '123' }, # optional
|
|
103
|
+
attachments: ['/path/to/file.pdf'] # optional: paths and/or IO objects, total <= 5 MB
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
client.messages.get(id: message['id']) # => includes 'status' and 'events'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Templates
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
template = client.templates.create(from_email: 'hello@world.com', subject: 'Hello [%name%]',
|
|
113
|
+
html: '<h1>Hello [%name%]</h1>', preset_params: ['name'])
|
|
114
|
+
client.templates.to_pending(id: template['id']) # submit for moderation
|
|
115
|
+
client.templates.list
|
|
116
|
+
client.templates.get(id: template['id'])
|
|
117
|
+
|
|
118
|
+
client.templates.deliver(template_id: template['id'], to: 'bob@example.org',
|
|
119
|
+
params: { 'name' => 'Bob' }, attachments: ['/path/to/file.pdf'])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Lists (recipient groups)
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
list = client.lists.create(title: 'My Recipients')
|
|
126
|
+
client.lists.list(page_number: 1, page_size: 100)
|
|
127
|
+
client.lists.get(id: list['id'])
|
|
128
|
+
client.lists.update(id: list['id'], title: 'New Title')
|
|
129
|
+
client.lists.delete(id: list['id']) # => true
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### List parameters
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
param = client.parameters.create(list_id: list['id'], title: 'Age', kind: 'numeric')
|
|
136
|
+
# kinds: string (default), numeric, date, boolean, geo
|
|
137
|
+
client.parameters.list(list_id: list['id'])
|
|
138
|
+
client.parameters.update(list_id: list['id'], id: param['id'], title: 'Years')
|
|
139
|
+
client.parameters.delete(list_id: list['id'], id: param['id']) # => true
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Recipients
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
recipient = client.recipients.create(
|
|
146
|
+
list_id: list['id'],
|
|
147
|
+
email: 'alice@example.org',
|
|
148
|
+
values: [{ parameter_id: param['id'], value: '22' }], # optional
|
|
149
|
+
tags: ['buyer'], # optional
|
|
150
|
+
unconfirmed: true # optional
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
client.recipients.get(list_id: list['id'], id: recipient['id'])
|
|
154
|
+
client.recipients.update(list_id: list['id'], id: recipient['id'],
|
|
155
|
+
email: 'alice@example.org',
|
|
156
|
+
values: [{ parameter_id: param['id'], destroy: 'true' }])
|
|
157
|
+
client.recipients.list(list_id: list['id'], page_size: 1000)
|
|
158
|
+
client.recipients.delete(list_id: list['id'], id: recipient['id']) # => true
|
|
159
|
+
|
|
160
|
+
# Bulk import (up to 10_000 recipients per call)
|
|
161
|
+
import = client.recipients.import(
|
|
162
|
+
list_id: list['id'],
|
|
163
|
+
recipients: [
|
|
164
|
+
{ email: 'alice@example.org', values: [{ parameter_id: param['id'], value: '22' }] },
|
|
165
|
+
{ email: 'bob@example.org' }
|
|
166
|
+
],
|
|
167
|
+
run_triggers: 'trigger_fresh', # optional: trigger_any | trigger_fresh
|
|
168
|
+
callback_url: 'https://my.app/import-callback' # optional
|
|
169
|
+
)
|
|
170
|
+
import['status'] # => 'queued'
|
|
171
|
+
|
|
172
|
+
# Find which lists contain an email
|
|
173
|
+
client.recipients.search(email: 'alice@example.org')
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Segments
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
client.segments.list
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Organizations
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
org = client.organizations.create(name: 'My Organization', address: 'Lenina 40',
|
|
186
|
+
country: 'Russia', city: 'Tomsk',
|
|
187
|
+
phone: '+7-3822-123-456', zip: '634000')
|
|
188
|
+
client.organizations.list
|
|
189
|
+
client.organizations.get(id: org['id'])
|
|
190
|
+
client.organizations.current # default organization
|
|
191
|
+
client.organizations.set_current(id: org['id'])
|
|
192
|
+
client.organizations.update(id: org['id'], city: 'Moscow')
|
|
193
|
+
client.organizations.delete(id: org['id']) # => true
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Campaigns
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
campaign = client.campaigns.create(
|
|
200
|
+
from_email: 'hello@world.com',
|
|
201
|
+
subject: 'Hello World',
|
|
202
|
+
html: '<h1>Hello World</h1>',
|
|
203
|
+
lists: [{ id: list['id'] }], # or segment_id: 5
|
|
204
|
+
attachments: ['/path/to/file'] # optional
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
client.campaigns.deliver(id: campaign['id']) # send now
|
|
208
|
+
client.campaigns.schedule(id: campaign['id'], # or send later
|
|
209
|
+
start_at: '30.10.2022 13:00', time_zone: 'Moscow')
|
|
210
|
+
client.campaigns.list(statistic: false) # statistic: false is much faster
|
|
211
|
+
client.campaigns.get(id: campaign['id'])
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Webhooks
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
hook = client.webhooks.create(title: 'Delivery hooks', url: 'https://my.app/webhook',
|
|
218
|
+
kinds: ['api'], events: %w[delivered hard_bounced])
|
|
219
|
+
client.webhooks.list
|
|
220
|
+
client.webhooks.get(id: hook['id'])
|
|
221
|
+
client.webhooks.update(id: hook['id'], status: 'inactive')
|
|
222
|
+
client.webhooks.delete(id: hook['id']) # => deleted webhook hash
|
|
223
|
+
|
|
224
|
+
client.webhooks.kinds # => { 'kinds' => ['campaign', 'campaign_transactional', ...] }
|
|
225
|
+
client.webhooks.events # => { 'events' => ['delivered', 'opened', ...] }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Pagination
|
|
229
|
+
|
|
230
|
+
Endpoints that return collections are wrapped in `Notisend::Collection`:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
lists = client.lists.list(page_size: 50)
|
|
234
|
+
lists.total_count # => 123
|
|
235
|
+
lists.total_pages # => 3
|
|
236
|
+
lists.page_number # => 1
|
|
237
|
+
lists.each { |l| puts l['title'] } # Enumerable over the current page
|
|
238
|
+
lists.next_page # => Collection or nil
|
|
239
|
+
|
|
240
|
+
lists.auto_paging_each { |l| puts l['title'] } # lazily walks ALL pages
|
|
241
|
+
client.lists.list.auto_paging_each.map { |l| l['id'] } # as an Enumerator
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The raw response envelope (including non-standard keys like `query` in recipient search)
|
|
245
|
+
is available via `collection.raw`.
|
|
246
|
+
|
|
247
|
+
## Error handling
|
|
248
|
+
|
|
249
|
+
All errors inherit from `Notisend::Error`:
|
|
250
|
+
|
|
251
|
+
| Class | Raised on |
|
|
252
|
+
| ------------------------------- | ------------------------------------- |
|
|
253
|
+
| `Notisend::ConfigurationError` | Missing API token |
|
|
254
|
+
| `Notisend::SSLError` | SSL verification failure |
|
|
255
|
+
| `Notisend::ConnectionError` | Network failure / timeout |
|
|
256
|
+
| `Notisend::ApiError` | Any non-2xx API response (base class) |
|
|
257
|
+
| `Notisend::BadRequest` | 400 |
|
|
258
|
+
| `Notisend::Unauthorized` | 401 |
|
|
259
|
+
| `Notisend::Forbidden` | 403 |
|
|
260
|
+
| `Notisend::NotFound` | 404 |
|
|
261
|
+
| `Notisend::PreconditionFailed` | 412 (e.g. `page_size` over the limit) |
|
|
262
|
+
| `Notisend::UnprocessableEntity` | 422 |
|
|
263
|
+
| `Notisend::TooManyRequests` | 429 (rate limit) |
|
|
264
|
+
| `Notisend::ServerError` | 5xx |
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
begin
|
|
268
|
+
client.messages.deliver(from_email: 'a@b.c', to: 'x@y.z', subject: 'Hi', text: 'Hello')
|
|
269
|
+
rescue Notisend::TooManyRequests => e
|
|
270
|
+
sleep(e.retry_after || 60) # parsed from 'Try again in N seconds'
|
|
271
|
+
retry
|
|
272
|
+
rescue Notisend::ApiError => e
|
|
273
|
+
e.status # => HTTP status
|
|
274
|
+
e.detail # => first error detail from the response
|
|
275
|
+
e.errors # => full parsed errors array
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
GET requests are automatically retried (twice) on connection failures and timeouts.
|
|
280
|
+
POST requests are never retried — a delivery must not be sent twice.
|
|
281
|
+
|
|
282
|
+
## Development
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
bundle install
|
|
286
|
+
bundle exec rake test # tests
|
|
287
|
+
bundle exec rubocop # lint
|
|
288
|
+
bundle exec rake # both
|
|
289
|
+
bin/console # interactive console
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Contributing
|
|
293
|
+
|
|
294
|
+
Bug reports and pull requests are welcome at <https://github.com/amdest/notisend>.
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
# HTTP core: builds Faraday connections, executes requests, maps errors
|
|
5
|
+
class Client
|
|
6
|
+
RETRY_OPTIONS = {
|
|
7
|
+
max: 2, interval: 0.2, backoff_factor: 2, methods: %i[get],
|
|
8
|
+
exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed, 'Timeout::Error', Errno::ETIMEDOUT]
|
|
9
|
+
}.freeze
|
|
10
|
+
DEFAULT_MIME_TYPE = 'application/octet-stream'
|
|
11
|
+
|
|
12
|
+
attr_reader :base_url
|
|
13
|
+
|
|
14
|
+
def initialize(api_token: nil, base_url: nil, open_timeout: nil, timeout: nil,
|
|
15
|
+
verify_ssl: nil, ca_bundle_file: nil, logger: nil)
|
|
16
|
+
defaults = Notisend.configuration
|
|
17
|
+
@api_token = api_token || defaults.api_token
|
|
18
|
+
@base_url = base_url || defaults.base_url
|
|
19
|
+
@open_timeout = open_timeout || defaults.open_timeout
|
|
20
|
+
@timeout = timeout || defaults.timeout
|
|
21
|
+
@verify_ssl = verify_ssl.nil? ? defaults.verify_ssl : verify_ssl
|
|
22
|
+
@ca_bundle_file = ca_bundle_file || defaults.ca_bundle_file
|
|
23
|
+
@logger = logger || defaults.logger
|
|
24
|
+
ensure_token!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def balance = @balance ||= Resources::Balance.new(self)
|
|
28
|
+
def messages = @messages ||= Resources::Messages.new(self)
|
|
29
|
+
def templates = @templates ||= Resources::Templates.new(self)
|
|
30
|
+
def lists = @lists ||= Resources::Lists.new(self)
|
|
31
|
+
def parameters = @parameters ||= Resources::Parameters.new(self)
|
|
32
|
+
def recipients = @recipients ||= Resources::Recipients.new(self)
|
|
33
|
+
def segments = @segments ||= Resources::Segments.new(self)
|
|
34
|
+
def organizations = @organizations ||= Resources::Organizations.new(self)
|
|
35
|
+
def campaigns = @campaigns ||= Resources::Campaigns.new(self)
|
|
36
|
+
def webhooks = @webhooks ||= Resources::Webhooks.new(self)
|
|
37
|
+
|
|
38
|
+
def get(path, params = {})
|
|
39
|
+
run_request { connection.get(path, params.compact) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def post(path, payload = {}, attachments: nil)
|
|
43
|
+
files = Array(attachments)
|
|
44
|
+
return run_request { connection.post(path, payload) } if files.empty?
|
|
45
|
+
|
|
46
|
+
body = payload.merge(attachments: files.map { |a| file_part(a) })
|
|
47
|
+
run_request { multipart_connection.post(path, body) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def patch(path, payload = {})
|
|
51
|
+
run_request { connection.patch(path, payload) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def delete(path)
|
|
55
|
+
run_request { connection.delete(path) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
attr_reader :api_token, :open_timeout, :timeout, :verify_ssl, :ca_bundle_file, :logger
|
|
61
|
+
|
|
62
|
+
def ensure_token!
|
|
63
|
+
return unless api_token.nil? || api_token.strip.empty?
|
|
64
|
+
|
|
65
|
+
raise ConfigurationError, 'Notisend API token is missing: set NOTISEND_API_TOKEN, ' \
|
|
66
|
+
'use Notisend.configure, or pass api_token:'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def connection
|
|
70
|
+
@connection ||= build_connection do |f|
|
|
71
|
+
f.request :json
|
|
72
|
+
f.request :retry, RETRY_OPTIONS
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def multipart_connection
|
|
77
|
+
@multipart_connection ||= build_connection do |f|
|
|
78
|
+
f.request :multipart
|
|
79
|
+
f.request :url_encoded
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def file_part(attachment)
|
|
84
|
+
if attachment.respond_to?(:read)
|
|
85
|
+
name = attachment.respond_to?(:path) ? File.basename(attachment.path) : 'attachment'
|
|
86
|
+
Faraday::Multipart::FilePart.new(attachment, DEFAULT_MIME_TYPE, name)
|
|
87
|
+
else
|
|
88
|
+
Faraday::Multipart::FilePart.new(attachment.to_s, DEFAULT_MIME_TYPE)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_connection
|
|
93
|
+
Faraday.new(url: url_prefix) do |f|
|
|
94
|
+
f.headers['Authorization'] = "Bearer #{api_token}"
|
|
95
|
+
f.headers['User-Agent'] = "notisend/#{VERSION}"
|
|
96
|
+
f.options.open_timeout = open_timeout
|
|
97
|
+
f.options.timeout = timeout
|
|
98
|
+
f.ssl.verify = verify_ssl
|
|
99
|
+
f.ssl.ca_file = ca_bundle_file if ca_bundle_file
|
|
100
|
+
yield(f)
|
|
101
|
+
f.response :logger, logger, headers: false, bodies: false if logger
|
|
102
|
+
f.adapter Faraday.default_adapter
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# trailing slash keeps the /v1 path segment when Faraday joins request paths
|
|
107
|
+
def url_prefix
|
|
108
|
+
"#{base_url.chomp('/')}/"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def run_request
|
|
112
|
+
handle(yield)
|
|
113
|
+
rescue Faraday::SSLError => e
|
|
114
|
+
raise SSLError, "SSL verification failed: #{e.message}"
|
|
115
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
|
|
116
|
+
raise ConnectionError, "Connection to Notisend API failed: #{e.message}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def handle(response)
|
|
120
|
+
raise ApiError.for(status: response.status, body: response.body) unless (200..299).cover?(response.status)
|
|
121
|
+
return true if response.status == 204 || response.body.to_s.strip.empty?
|
|
122
|
+
|
|
123
|
+
JSON.parse(response.body)
|
|
124
|
+
rescue JSON::ParserError
|
|
125
|
+
raise Error, "Notisend API returned invalid JSON (status #{response.status})"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
# Wraps paginated API responses; re-fetches further pages through the block
|
|
5
|
+
class Collection
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :raw
|
|
9
|
+
|
|
10
|
+
def initialize(raw, &fetch_page)
|
|
11
|
+
@raw = raw
|
|
12
|
+
@fetch_page = fetch_page
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def items = Array(raw['collection'])
|
|
16
|
+
def total_count = raw['total_count']
|
|
17
|
+
def total_pages = raw['total_pages']
|
|
18
|
+
def page_number = raw['page_number']
|
|
19
|
+
def page_size = raw['page_size']
|
|
20
|
+
|
|
21
|
+
def each(&) = items.each(&)
|
|
22
|
+
|
|
23
|
+
def next_page?
|
|
24
|
+
!!(page_number && total_pages && page_number < total_pages)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def next_page
|
|
28
|
+
return unless next_page? && @fetch_page
|
|
29
|
+
|
|
30
|
+
@fetch_page.call(page_number + 1)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def auto_paging_each(&block)
|
|
34
|
+
return enum_for(:auto_paging_each) unless block
|
|
35
|
+
|
|
36
|
+
page = self
|
|
37
|
+
while page
|
|
38
|
+
page.each(&block)
|
|
39
|
+
page = page.next_page
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
# Holds client settings; every option can be overridden per Client instance
|
|
5
|
+
class Configuration
|
|
6
|
+
DEFAULT_BASE_URL = 'https://api.notisend.ru/v1'
|
|
7
|
+
|
|
8
|
+
attr_accessor :api_token, :base_url, :open_timeout, :timeout, :verify_ssl, :ca_bundle_file, :logger
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@api_token = ENV.fetch('NOTISEND_API_TOKEN', nil)
|
|
12
|
+
@base_url = DEFAULT_BASE_URL
|
|
13
|
+
@open_timeout = 10
|
|
14
|
+
@timeout = 30
|
|
15
|
+
@verify_ssl = ENV.fetch('NOTISEND_VERIFY_SSL', 'true') != 'false'
|
|
16
|
+
@ca_bundle_file = ENV.fetch('NOTISEND_CA_BUNDLE_FILE', nil)
|
|
17
|
+
@logger = nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Notisend
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class ConfigurationError < Error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class SSLError < Error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class ConnectionError < Error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Non-2xx API response. Body format: {"errors":[{"code":..,"detail":".."}]}
|
|
19
|
+
class ApiError < Error
|
|
20
|
+
STATUS_MAP = {
|
|
21
|
+
400 => 'Notisend::BadRequest',
|
|
22
|
+
401 => 'Notisend::Unauthorized',
|
|
23
|
+
403 => 'Notisend::Forbidden',
|
|
24
|
+
404 => 'Notisend::NotFound',
|
|
25
|
+
412 => 'Notisend::PreconditionFailed',
|
|
26
|
+
422 => 'Notisend::UnprocessableEntity',
|
|
27
|
+
429 => 'Notisend::TooManyRequests'
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :status, :errors
|
|
31
|
+
|
|
32
|
+
def self.for(status:, body:)
|
|
33
|
+
klass_name = STATUS_MAP[status] || ((500..599).cover?(status) ? 'Notisend::ServerError' : name)
|
|
34
|
+
Object.const_get(klass_name).new(status: status, body: body)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(status:, body:)
|
|
38
|
+
@status = status
|
|
39
|
+
@errors = parse_errors(body)
|
|
40
|
+
super(detail || "Notisend API error (status #{status})")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def detail
|
|
44
|
+
errors.first&.fetch('detail', nil)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def parse_errors(body)
|
|
50
|
+
return [] if body.nil? || (body.respond_to?(:empty?) && body.empty?)
|
|
51
|
+
|
|
52
|
+
parsed = body.is_a?(String) ? JSON.parse(body) : body
|
|
53
|
+
parsed.is_a?(Hash) ? Array(parsed['errors']) : []
|
|
54
|
+
rescue JSON::ParserError
|
|
55
|
+
[{ 'code' => status, 'detail' => body.to_s }]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class BadRequest < ApiError
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Unauthorized < ApiError
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class Forbidden < ApiError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class NotFound < ApiError
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class PreconditionFailed < ApiError
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class UnprocessableEntity < ApiError
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class ServerError < ApiError
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 429: exposes the server-suggested wait extracted from the detail text
|
|
81
|
+
class TooManyRequests < ApiError
|
|
82
|
+
def retry_after
|
|
83
|
+
match = detail&.match(/(\d+) seconds?/)
|
|
84
|
+
match && match[1].to_i
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# Shared plumbing for API resources
|
|
6
|
+
class Base
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
attr_reader :client
|
|
14
|
+
|
|
15
|
+
def email_path(*segments)
|
|
16
|
+
['email', *segments].join('/')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def paginated(path, params = {})
|
|
20
|
+
params = params.compact
|
|
21
|
+
response = client.get(path, params)
|
|
22
|
+
Collection.new(response) { |page| paginated(path, params.merge(page_number: page)) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def page_params(page_number: nil, page_size: nil)
|
|
26
|
+
{ page_number: page_number, page_size: page_size }.compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# Bulk campaigns: /email/campaigns
|
|
6
|
+
class Campaigns < Base
|
|
7
|
+
def create(from_email:, subject:, html:, text: nil, from_name: nil,
|
|
8
|
+
lists: nil, segment_id: nil, attachments: nil)
|
|
9
|
+
payload = { from_email:, subject:, html:, text:, from_name:, lists:, segment_id: }.compact
|
|
10
|
+
client.post(email_path('campaigns'), payload, attachments: attachments)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def deliver(id:)
|
|
14
|
+
client.patch(email_path('campaigns', id, 'deliver'))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def schedule(id:, start_at: nil, time_zone: nil)
|
|
18
|
+
client.patch(email_path('campaigns', id, 'schedule'), { start_at:, time_zone: }.compact)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def list(statistic: nil, page_number: nil, page_size: nil)
|
|
22
|
+
params = { statistic: }.compact.merge(page_params(page_number:, page_size:))
|
|
23
|
+
paginated(email_path('campaigns'), params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get(id:)
|
|
27
|
+
client.get(email_path('campaigns', id))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# Recipient lists (groups): /email/lists
|
|
6
|
+
class Lists < Base
|
|
7
|
+
def create(title:)
|
|
8
|
+
client.post(email_path('lists'), { title: })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def list(page_number: nil, page_size: nil)
|
|
12
|
+
paginated(email_path('lists'), page_params(page_number:, page_size:))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(id:)
|
|
16
|
+
client.get(email_path('lists', id))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update(id:, title:)
|
|
20
|
+
client.patch(email_path('lists', id), { title: })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete(id:)
|
|
24
|
+
client.delete(email_path('lists', id))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# Single email messages: POST /email/messages, GET /email/messages/:id
|
|
6
|
+
class Messages < Base
|
|
7
|
+
def deliver(from_email:, to:, subject:, text: nil, html: nil, from_name: nil,
|
|
8
|
+
payment: nil, smtp_headers: nil, attachments: nil)
|
|
9
|
+
payload = { from_email:, to:, subject:, text:, html:, from_name:,
|
|
10
|
+
payment:, smtp_headers: }.compact
|
|
11
|
+
client.post(email_path('messages'), payload, attachments: attachments)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get(id:)
|
|
15
|
+
client.get(email_path('messages', id))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# Sender organizations: /email/organizations
|
|
6
|
+
class Organizations < Base
|
|
7
|
+
def create(name:, address:, country:, city:, phone:, zip:)
|
|
8
|
+
client.post(email_path('organizations'), { name:, address:, country:, city:, phone:, zip: })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def list(page_number: nil, page_size: nil)
|
|
12
|
+
paginated(email_path('organizations'), page_params(page_number:, page_size:))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(id:)
|
|
16
|
+
client.get(email_path('organizations', id))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def current
|
|
20
|
+
client.get(email_path('organizations', 'current'))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def set_current(id:)
|
|
24
|
+
client.patch(email_path('organizations', id, 'current'))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update(id:, name: nil, address: nil, country: nil, city: nil, phone: nil, zip: nil)
|
|
28
|
+
payload = { name:, address:, country:, city:, phone:, zip: }.compact
|
|
29
|
+
client.patch(email_path('organizations', id), payload)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete(id:)
|
|
33
|
+
client.delete(email_path('organizations', id))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# List parameters: /email/lists/:list_id/parameters
|
|
6
|
+
class Parameters < Base
|
|
7
|
+
def create(list_id:, title:, kind: nil)
|
|
8
|
+
client.post(email_path('lists', list_id, 'parameters'), { title:, kind: }.compact)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def list(list_id:, page_number: nil, page_size: nil)
|
|
12
|
+
paginated(email_path('lists', list_id, 'parameters'), page_params(page_number:, page_size:))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update(list_id:, id:, title: nil, kind: nil)
|
|
16
|
+
client.patch(email_path('lists', list_id, 'parameters', id), { title:, kind: }.compact)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def delete(list_id:, id:)
|
|
20
|
+
client.delete(email_path('lists', list_id, 'parameters', id))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# List recipients: /email/lists/:list_id/recipients (+ global /email/recipients/search)
|
|
6
|
+
class Recipients < Base
|
|
7
|
+
def create(list_id:, email:, values: nil, tags: nil, unconfirmed: nil)
|
|
8
|
+
payload = { email:, values:, tags:, unconfirmed: }.compact
|
|
9
|
+
client.post(email_path('lists', list_id, 'recipients'), payload)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def get(list_id:, id:)
|
|
13
|
+
client.get(email_path('lists', list_id, 'recipients', id))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update(list_id:, id:, email:, values: nil, tags: nil, run_triggers: nil)
|
|
17
|
+
payload = { email:, values:, tags:, run_triggers: }.compact
|
|
18
|
+
client.patch(email_path('lists', list_id, 'recipients', id), payload)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def list(list_id:, page_number: nil, page_size: nil)
|
|
22
|
+
paginated(email_path('lists', list_id, 'recipients'), page_params(page_number:, page_size:))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete(list_id:, id:)
|
|
26
|
+
client.delete(email_path('lists', list_id, 'recipients', id))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def import(list_id:, recipients:, run_triggers: nil, tags: nil, callback_url: nil)
|
|
30
|
+
payload = { recipients:, run_triggers:, tags:, callback_url: }.compact
|
|
31
|
+
client.post(email_path('lists', list_id, 'recipients', 'imports'), payload)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def search(email:, page_number: nil, page_size: nil)
|
|
35
|
+
params = { email: }.merge(page_params(page_number:, page_size:))
|
|
36
|
+
paginated(email_path('recipients', 'search'), params)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# Segments: GET /email/segments
|
|
6
|
+
class Segments < Base
|
|
7
|
+
def list(page_number: nil, page_size: nil)
|
|
8
|
+
paginated(email_path('segments'), page_params(page_number:, page_size:))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# Email templates and template-based sending: /email/templates
|
|
6
|
+
class Templates < Base
|
|
7
|
+
def create(from_email:, subject:, html:, text: nil, from_name: nil, name: nil, preset_params: nil)
|
|
8
|
+
payload = { from_email:, subject:, html:, text:, from_name:, name:, preset_params: }.compact
|
|
9
|
+
client.post(email_path('templates'), payload)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def list(page_number: nil, page_size: nil)
|
|
13
|
+
paginated(email_path('templates'), page_params(page_number:, page_size:))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(id:)
|
|
17
|
+
client.get(email_path('templates', id))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_pending(id:)
|
|
21
|
+
client.patch(email_path('templates', id, 'to_pending'))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def deliver(template_id:, to:, params: nil, payment: nil, attachments: nil)
|
|
25
|
+
payload = { to:, params:, payment: }.compact
|
|
26
|
+
client.post(email_path('templates', template_id, 'messages'), payload, attachments: attachments)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notisend
|
|
4
|
+
module Resources
|
|
5
|
+
# Message webhooks: /email/messages_webhooks
|
|
6
|
+
class Webhooks < Base
|
|
7
|
+
def create(title:, url:, kinds:, events:)
|
|
8
|
+
client.post(email_path('messages_webhooks'), { title:, url:, kinds:, events: })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def list(page_number: nil, page_size: nil)
|
|
12
|
+
paginated(email_path('messages_webhooks'), page_params(page_number:, page_size:))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(id:)
|
|
16
|
+
client.get(email_path('messages_webhooks', id))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update(id:, title: nil, url: nil, kinds: nil, events: nil, status: nil)
|
|
20
|
+
payload = { title:, url:, kinds:, events:, status: }.compact
|
|
21
|
+
client.patch(email_path('messages_webhooks', id), payload)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def delete(id:)
|
|
25
|
+
client.delete(email_path('messages_webhooks', id))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def kinds
|
|
29
|
+
client.get(email_path('messages_webhooks', 'kinds'))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def events
|
|
33
|
+
client.get(email_path('messages_webhooks', 'events'))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/notisend.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'faraday/multipart'
|
|
5
|
+
require 'faraday/retry'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
require_relative 'notisend/version'
|
|
9
|
+
require_relative 'notisend/errors'
|
|
10
|
+
require_relative 'notisend/configuration'
|
|
11
|
+
require_relative 'notisend/collection'
|
|
12
|
+
require_relative 'notisend/client'
|
|
13
|
+
require_relative 'notisend/resources/base'
|
|
14
|
+
require_relative 'notisend/resources/balance'
|
|
15
|
+
require_relative 'notisend/resources/messages'
|
|
16
|
+
require_relative 'notisend/resources/templates'
|
|
17
|
+
require_relative 'notisend/resources/lists'
|
|
18
|
+
require_relative 'notisend/resources/parameters'
|
|
19
|
+
require_relative 'notisend/resources/recipients'
|
|
20
|
+
require_relative 'notisend/resources/segments'
|
|
21
|
+
require_relative 'notisend/resources/organizations'
|
|
22
|
+
require_relative 'notisend/resources/campaigns'
|
|
23
|
+
require_relative 'notisend/resources/webhooks'
|
|
24
|
+
|
|
25
|
+
# Ruby client for the Notisend email API (https://notisend.ru/dev/email/api/)
|
|
26
|
+
module Notisend
|
|
27
|
+
RESERVE_BASE_URL = 'https://api-reserve.msndr.net/v1'
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def configuration
|
|
31
|
+
@configuration ||= Configuration.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def configure
|
|
35
|
+
yield(configuration) if block_given?
|
|
36
|
+
configuration
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def client
|
|
40
|
+
@client ||= Client.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset
|
|
44
|
+
@configuration = nil
|
|
45
|
+
@client = nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: notisend
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Aleksandr Dryzhuk
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.13'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.13'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-multipart
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: faraday-retry
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.3'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.3'
|
|
54
|
+
description: 'Full-coverage Ruby client for the Notisend email API: single messages,
|
|
55
|
+
templates, recipient lists, parameters, recipients, segments, organizations, campaigns,
|
|
56
|
+
webhooks and balance. Faraday-based, with pagination helpers, typed errors and custom
|
|
57
|
+
SSL certificate support.'
|
|
58
|
+
email:
|
|
59
|
+
- dev@ad-it.pro
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- CHANGELOG.md
|
|
65
|
+
- LICENSE.txt
|
|
66
|
+
- README.md
|
|
67
|
+
- lib/notisend.rb
|
|
68
|
+
- lib/notisend/client.rb
|
|
69
|
+
- lib/notisend/collection.rb
|
|
70
|
+
- lib/notisend/configuration.rb
|
|
71
|
+
- lib/notisend/errors.rb
|
|
72
|
+
- lib/notisend/resources/balance.rb
|
|
73
|
+
- lib/notisend/resources/base.rb
|
|
74
|
+
- lib/notisend/resources/campaigns.rb
|
|
75
|
+
- lib/notisend/resources/lists.rb
|
|
76
|
+
- lib/notisend/resources/messages.rb
|
|
77
|
+
- lib/notisend/resources/organizations.rb
|
|
78
|
+
- lib/notisend/resources/parameters.rb
|
|
79
|
+
- lib/notisend/resources/recipients.rb
|
|
80
|
+
- lib/notisend/resources/segments.rb
|
|
81
|
+
- lib/notisend/resources/templates.rb
|
|
82
|
+
- lib/notisend/resources/webhooks.rb
|
|
83
|
+
- lib/notisend/version.rb
|
|
84
|
+
homepage: https://github.com/amdest/notisend
|
|
85
|
+
licenses:
|
|
86
|
+
- MIT
|
|
87
|
+
metadata:
|
|
88
|
+
allowed_push_host: https://rubygems.org
|
|
89
|
+
homepage_uri: https://github.com/amdest/notisend
|
|
90
|
+
source_code_uri: https://github.com/amdest/notisend
|
|
91
|
+
changelog_uri: https://github.com/amdest/notisend/blob/master/CHANGELOG.md
|
|
92
|
+
documentation_uri: https://github.com/amdest/notisend/blob/master/README.md
|
|
93
|
+
bug_tracker_uri: https://github.com/amdest/notisend/issues
|
|
94
|
+
rubygems_mfa_required: 'true'
|
|
95
|
+
rdoc_options: []
|
|
96
|
+
require_paths:
|
|
97
|
+
- lib
|
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 3.4.0
|
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '0'
|
|
108
|
+
requirements: []
|
|
109
|
+
rubygems_version: 4.0.18
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: Ruby client for the Notisend email API
|
|
112
|
+
test_files: []
|