courrier 0.11.0 → 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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +76 -69
- data/courrier.gemspec +1 -1
- data/lib/courrier/configuration.rb +0 -24
- data/lib/courrier/email/provider.rb +2 -0
- data/lib/courrier/email/providers/base.rb +2 -0
- data/lib/courrier/email/providers/brevo.rb +36 -0
- data/lib/courrier/email/providers/cloudflare.rb +2 -0
- data/lib/courrier/email/providers/lettermint.rb +2 -0
- data/lib/courrier/email/providers/loops.rb +2 -0
- data/lib/courrier/email/providers/mailgun.rb +2 -0
- data/lib/courrier/email/providers/mailjet.rb +2 -0
- data/lib/courrier/email/providers/postmark.rb +2 -0
- data/lib/courrier/email/providers/ses.rb +2 -0
- data/lib/courrier/email/providers/userlist.rb +2 -0
- data/lib/courrier/email.rb +10 -4
- data/lib/courrier/subscriber/brevo.rb +39 -0
- data/lib/courrier/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0a97b8804b294b065433077d025cde44716b87821214649a6421cac278beca5
|
|
4
|
+
data.tar.gz: 814110bf00d835bfacb15bc8f59bdeb04a0cb7f7b572d4418c3fd901e98c1115
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc4503ec4cef48dbd12f67f093ccb3df9b29834d34b26f99af90b8020f9be2f7207b7629be79d50ba90af9b7b01c47e104568c1813b7b5296211ed788f453831
|
|
7
|
+
data.tar.gz: d50436d4e7417f481f3f2ad2cc59962b30f42fd60d471f7226128e02e04c0378c117968a8c64c8346a735af2e1dae843d1d0447bdb9ce91153850d9e67af1d83
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -121,12 +121,10 @@ OrderEmail.deliver to: "recipient@railsdesigner.com",\
|
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
Provider and API key settings can be overridden using environment variables (`COURRIER_PROVIDER` and `COURRIER_API_KEY`) for both global configuration and email class defaults.
|
|
125
|
-
|
|
126
124
|
|
|
127
125
|
## Custom headers
|
|
128
126
|
|
|
129
|
-
Email classes can define custom
|
|
127
|
+
Email classes can define custom headers that are sent with every email:
|
|
130
128
|
```ruby
|
|
131
129
|
class OrderEmail < Courrier::Email
|
|
132
130
|
headers list_unsubscribe_post: "List-Unsubscribe=One-Click"
|
|
@@ -194,6 +192,7 @@ end
|
|
|
194
192
|
Courrier supports these transactional email providers:
|
|
195
193
|
|
|
196
194
|
- [AWS SES](https://aws.amazon.com/ses/) — requires `aws-sigv4` gem
|
|
195
|
+
- [Brevo](https://www.brevo.com/)
|
|
197
196
|
- [Cloudflare Email Service](https://developers.cloudflare.com/email-service/)
|
|
198
197
|
- [Lettermint](https://lettermint.co)
|
|
199
198
|
- [Loops](https://loops.so)
|
|
@@ -209,56 +208,14 @@ Courrier supports these transactional email providers:
|
|
|
209
208
|
|
|
210
209
|
## More Features
|
|
211
210
|
|
|
212
|
-
Additional functionality to
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
### Layout support
|
|
216
|
-
|
|
217
|
-
Wrap your email content using layouts:
|
|
218
|
-
```ruby
|
|
219
|
-
class OrderEmail < Courrier::Email
|
|
220
|
-
layout text: "%{content}\n\nThanks for your order!",
|
|
221
|
-
html: "<div>\n%{content}\n</div>"
|
|
222
|
-
end
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Using a method:
|
|
226
|
-
```ruby
|
|
227
|
-
class OrderEmail < Courrier::Email
|
|
228
|
-
layout html: :html_layout
|
|
229
|
-
|
|
230
|
-
def html_layout
|
|
231
|
-
<<~HTML
|
|
232
|
-
<div style='font-family: ui-sans-serif, system-ui;'>
|
|
233
|
-
%{content}
|
|
234
|
-
</div>
|
|
235
|
-
HTML
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
Using a separate class:
|
|
241
|
-
```ruby
|
|
242
|
-
class OrderEmail < Courrier::Email
|
|
243
|
-
layout html: OrderLayout
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
class OrderLayout
|
|
247
|
-
self.call
|
|
248
|
-
<<~HTML
|
|
249
|
-
<div style='font-family: ui-sans-serif, system-ui;'>
|
|
250
|
-
%{content}
|
|
251
|
-
</div>
|
|
252
|
-
HTML
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
```
|
|
211
|
+
Additional functionality to go beyond the basics:
|
|
256
212
|
|
|
257
213
|
|
|
258
214
|
### Template files
|
|
259
215
|
|
|
260
216
|
Instead of defining `text` and `html` methods, you can create ERB template files:
|
|
261
217
|
```ruby
|
|
218
|
+
# courrier/emails/order_email.rb
|
|
262
219
|
class OrderEmail < Courrier::Email
|
|
263
220
|
def subject = "Your order is ready!"
|
|
264
221
|
|
|
@@ -291,7 +248,7 @@ end
|
|
|
291
248
|
|
|
292
249
|
## Markdown support
|
|
293
250
|
|
|
294
|
-
Courrier supports rendering markdown
|
|
251
|
+
Courrier supports rendering markdown to HTML. Bundle any supported markdown gem (`redcarpet`, `kramdown` or `commonmarker`) and it will be used.
|
|
295
252
|
|
|
296
253
|
|
|
297
254
|
### Markdown methods
|
|
@@ -336,6 +293,49 @@ Your order **#<%= order_id %>** is ready for pickup.
|
|
|
336
293
|
Method definitions take precedence over template files. You can mix approaches. For example, define `text` in a method and use a markdown template for HTML content.
|
|
337
294
|
|
|
338
295
|
|
|
296
|
+
### Layout support
|
|
297
|
+
|
|
298
|
+
Wrap your email content using layouts:
|
|
299
|
+
```ruby
|
|
300
|
+
class OrderEmail < Courrier::Email
|
|
301
|
+
layout text: "%{content}\n\nThanks for your order!",
|
|
302
|
+
html: "<div>\n%{content}\n</div>"
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Using a method:
|
|
307
|
+
```ruby
|
|
308
|
+
class OrderEmail < Courrier::Email
|
|
309
|
+
layout html: :html_layout
|
|
310
|
+
|
|
311
|
+
def html_layout
|
|
312
|
+
<<~HTML
|
|
313
|
+
<div style='font-family: ui-sans-serif, system-ui;'>
|
|
314
|
+
%{content}
|
|
315
|
+
</div>
|
|
316
|
+
HTML
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Using a separate class:
|
|
322
|
+
```ruby
|
|
323
|
+
class OrderEmail < Courrier::Email
|
|
324
|
+
layout html: OrderLayout
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
class OrderLayout
|
|
328
|
+
self.call
|
|
329
|
+
<<~HTML
|
|
330
|
+
<div style='font-family: ui-sans-serif, system-ui;'>
|
|
331
|
+
%{content}
|
|
332
|
+
</div>
|
|
333
|
+
HTML
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
|
|
339
339
|
### Auto-generate text from HTML
|
|
340
340
|
|
|
341
341
|
Automatically generate plain text versions from your HTML emails:
|
|
@@ -344,6 +344,22 @@ config.auto_generate_text = true # defaults to false
|
|
|
344
344
|
```
|
|
345
345
|
|
|
346
346
|
|
|
347
|
+
### Delivery callbacks
|
|
348
|
+
|
|
349
|
+
Hook into the email delivery lifecycle with `before_deliver` and `after_deliver` callbacks:
|
|
350
|
+
```ruby
|
|
351
|
+
class OrderEmail < Courrier::Email
|
|
352
|
+
before_deliver do |email|
|
|
353
|
+
puts "Sending to #{email.options.to}" # access email options, abort delivery by returning false
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
after_deliver do |email, result|
|
|
357
|
+
puts "Delivered: #{result.success?}" # access email and delivery result
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
|
|
347
363
|
### Email address helper
|
|
348
364
|
|
|
349
365
|
Compose email addresses with display names:
|
|
@@ -365,7 +381,7 @@ end
|
|
|
365
381
|
Use Ruby's built-in Logger for development and testing:
|
|
366
382
|
|
|
367
383
|
```ruby
|
|
368
|
-
config.
|
|
384
|
+
config.email = { provider: "logger" } # outputs emails to STDOUT
|
|
369
385
|
config.logger = custom_logger # optional: defaults to ::Logger.new($stdout)
|
|
370
386
|
```
|
|
371
387
|
|
|
@@ -385,7 +401,7 @@ end
|
|
|
385
401
|
|
|
386
402
|
Then configure it:
|
|
387
403
|
```ruby
|
|
388
|
-
config.
|
|
404
|
+
config.email = { provider: "CustomProvider" }
|
|
389
405
|
```
|
|
390
406
|
|
|
391
407
|
Check the [existing providers](https://github.com/Rails-Designer/courrier/tree/main/lib/courrier/email/providers) for implementation examples.
|
|
@@ -449,27 +465,9 @@ Available assertions:
|
|
|
449
465
|
- `assert_email_delivered(email_class, to:, from:, subject:, provider:)`; assert an email matching criteria was delivered
|
|
450
466
|
|
|
451
467
|
|
|
452
|
-
### Delivery callbacks
|
|
453
|
-
|
|
454
|
-
Hook into the email delivery lifecycle with `before_deliver` and `after_deliver` callbacks:
|
|
455
|
-
```ruby
|
|
456
|
-
class OrderEmail < Courrier::Email
|
|
457
|
-
before_deliver do |email|
|
|
458
|
-
puts "Sending to #{email.options.to}" # access email options, abort delivery by returning false
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
after_deliver do |email, result|
|
|
462
|
-
puts "Delivered: #{result.success?}" # access email and delivery result
|
|
463
|
-
end
|
|
464
|
-
end
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
Callbacks are isolated per class (subclasses don't inherit parent callbacks).
|
|
468
|
-
|
|
469
|
-
|
|
470
468
|
## Newsletter subscriptions
|
|
471
469
|
|
|
472
|
-
|
|
470
|
+
Use Courrier to manage subscribers across popular email marketing platforms:
|
|
473
471
|
```ruby
|
|
474
472
|
Courrier.configure do |config|
|
|
475
473
|
config.subscriber = {
|
|
@@ -497,6 +495,7 @@ end
|
|
|
497
495
|
### Supported providers
|
|
498
496
|
|
|
499
497
|
- [Beehiiv](https://www.beehiiv.com/) - requires `publication_id`
|
|
498
|
+
- [Brevo](https://www.brevo.com/) - accepts optional `list_ids`
|
|
500
499
|
- [Buttondown](https://buttondown.com)
|
|
501
500
|
- [Kit](https://kit.com/) (formerly ConvertKit) - requires `form_id`
|
|
502
501
|
- [Loops](https://loops.so/)
|
|
@@ -513,6 +512,14 @@ config.subscriber = {
|
|
|
513
512
|
}
|
|
514
513
|
```
|
|
515
514
|
|
|
515
|
+
For Brevo, pass one or more list IDs when new contacts should be added to specific lists:
|
|
516
|
+
```ruby
|
|
517
|
+
config.subscriber = {
|
|
518
|
+
provider: "brevo",
|
|
519
|
+
api_key: "your_api_key",
|
|
520
|
+
list_ids: [12, 34]
|
|
521
|
+
}
|
|
522
|
+
```
|
|
516
523
|
|
|
517
524
|
### Custom providers
|
|
518
525
|
|
data/courrier.gemspec
CHANGED
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
|
|
19
19
|
spec.files = Dir["{bin,lib}/**/*", "Rakefile", "README.md", "courrier.gemspec", "Gemfile", "Gemfile.lock"]
|
|
20
20
|
|
|
21
|
-
spec.required_ruby_version = ">= 4.0
|
|
21
|
+
spec.required_ruby_version = ">= 3.4.0"
|
|
22
22
|
|
|
23
23
|
spec.add_dependency "logger", ">= 1.5", "< 3"
|
|
24
24
|
spec.add_dependency "nokogiri", ">= 1.18", "< 2"
|
|
@@ -42,30 +42,6 @@ module Courrier
|
|
|
42
42
|
@providers = Courrier::Configuration::Providers.new
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
def provider
|
|
46
|
-
warn "[DEPRECATION] `provider` is deprecated. Use `email = { provider: '…' }` instead. Will be removed in 1.0.0"
|
|
47
|
-
|
|
48
|
-
@email[:provider]
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def provider=(value)
|
|
52
|
-
warn "[DEPRECATION] `provider=` is deprecated. Use `email = { provider: '…' }` instead. Will be removed in 1.0.0"
|
|
53
|
-
|
|
54
|
-
@email[:provider] = value
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def api_key
|
|
58
|
-
warn "[DEPRECATION] `api_key` is deprecated. Use `email = { api_key: '…' }` instead. Will be removed in 1.0.0"
|
|
59
|
-
|
|
60
|
-
@email[:api_key]
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def api_key=(value)
|
|
64
|
-
warn "[DEPRECATION] `api_key=` is deprecated. Use `email = { api_key: '…' }` instead. Will be removed in 1.0.0"
|
|
65
|
-
|
|
66
|
-
@email[:api_key] = value
|
|
67
|
-
end
|
|
68
|
-
|
|
69
45
|
private
|
|
70
46
|
|
|
71
47
|
def default_email_path
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "courrier/email/providers/base"
|
|
4
|
+
require "courrier/email/providers/brevo"
|
|
4
5
|
require "courrier/email/providers/cloudflare"
|
|
5
6
|
require "courrier/email/providers/lettermint"
|
|
6
7
|
require "courrier/email/providers/logger"
|
|
@@ -20,6 +21,7 @@ module Courrier
|
|
|
20
21
|
class Email
|
|
21
22
|
class Provider
|
|
22
23
|
PROVIDERS = {
|
|
24
|
+
brevo: Courrier::Email::Providers::Brevo,
|
|
23
25
|
cloudflare: Courrier::Email::Providers::Cloudflare,
|
|
24
26
|
logger: Courrier::Email::Providers::Logger,
|
|
25
27
|
lettermint: Courrier::Email::Providers::Lettermint,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Courrier
|
|
4
|
+
class Email
|
|
5
|
+
module Providers
|
|
6
|
+
class Brevo < Base
|
|
7
|
+
ENDPOINT_URL = "https://api.brevo.com/v3/smtp/email"
|
|
8
|
+
|
|
9
|
+
def body
|
|
10
|
+
{
|
|
11
|
+
"sender" => email_address(@options.from),
|
|
12
|
+
"to" => email_addresses(@options.to),
|
|
13
|
+
"cc" => email_addresses(@options.cc),
|
|
14
|
+
"bcc" => email_addresses(@options.bcc),
|
|
15
|
+
"replyTo" => email_address(@options.reply_to),
|
|
16
|
+
"subject" => @options.subject,
|
|
17
|
+
"htmlContent" => @options.html,
|
|
18
|
+
"textContent" => @options.text
|
|
19
|
+
}.compact
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def default_headers = {"api-key" => @api_key}
|
|
25
|
+
|
|
26
|
+
def email_addresses(addresses)
|
|
27
|
+
addresses&.split(",")&.map { |address| email_address(address) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def email_address(address)
|
|
31
|
+
{"email" => address.strip} if address
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -4,6 +4,8 @@ module Courrier
|
|
|
4
4
|
class Email
|
|
5
5
|
module Providers
|
|
6
6
|
class Ses < Base
|
|
7
|
+
def self.config_options = %w[region access_key_id secret_access_key session_token]
|
|
8
|
+
|
|
7
9
|
def deliver
|
|
8
10
|
uri = URI.parse("https://email.#{@provider_options[:region]}.amazonaws.com/v2/email/outbound-emails")
|
|
9
11
|
|
data/lib/courrier/email.rb
CHANGED
|
@@ -60,11 +60,17 @@ module Courrier
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def before_deliver_callbacks
|
|
63
|
-
@before_deliver
|
|
63
|
+
return @before_deliver if @before_deliver
|
|
64
|
+
return superclass.before_deliver_callbacks if superclass.respond_to?(:before_deliver_callbacks)
|
|
65
|
+
|
|
66
|
+
[]
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def after_deliver_callbacks
|
|
67
|
-
@after_deliver
|
|
70
|
+
return @after_deliver if @after_deliver
|
|
71
|
+
return superclass.after_deliver_callbacks if superclass.respond_to?(:after_deliver_callbacks)
|
|
72
|
+
|
|
73
|
+
[]
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def deliver(**options)
|
|
@@ -78,8 +84,8 @@ module Courrier
|
|
|
78
84
|
end
|
|
79
85
|
|
|
80
86
|
def initialize(options = {})
|
|
81
|
-
@provider = options[:provider] ||
|
|
82
|
-
@api_key = options[:api_key] ||
|
|
87
|
+
@provider = options[:provider] || self.class.provider || Courrier.configuration&.email&.[](:provider)
|
|
88
|
+
@api_key = options[:api_key] || self.class.api_key || Courrier.configuration&.email&.[](:api_key)
|
|
83
89
|
|
|
84
90
|
@default_url_options = self.class.default_url_options.merge(options[:default_url_options] || {})
|
|
85
91
|
@context_options = options.except(:provider, :api_key, :from, :to, :reply_to, :cc, :bcc, :subject, :text, :html)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "courrier/subscriber/base"
|
|
5
|
+
|
|
6
|
+
module Courrier
|
|
7
|
+
class Subscriber
|
|
8
|
+
class Brevo < Base
|
|
9
|
+
ENDPOINT_URL = "https://api.brevo.com/v3/contacts"
|
|
10
|
+
|
|
11
|
+
def create(email)
|
|
12
|
+
body = {
|
|
13
|
+
"email" => email,
|
|
14
|
+
"updateEnabled" => true
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
list_ids = Array(Courrier.configuration.subscriber[:list_ids]).compact
|
|
18
|
+
body["listIds"] = list_ids unless list_ids.empty?
|
|
19
|
+
|
|
20
|
+
request(:post, ENDPOINT_URL, body)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def destroy(email)
|
|
24
|
+
identifier = URI.encode_www_form_component(email)
|
|
25
|
+
|
|
26
|
+
request(:delete, "#{ENDPOINT_URL}/#{identifier}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def headers
|
|
32
|
+
{
|
|
33
|
+
"api-key" => @api_key,
|
|
34
|
+
"Content-Type" => "application/json"
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/courrier/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: courrier
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rails Designer
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- lib/courrier/email/options.rb
|
|
75
75
|
- lib/courrier/email/provider.rb
|
|
76
76
|
- lib/courrier/email/providers/base.rb
|
|
77
|
+
- lib/courrier/email/providers/brevo.rb
|
|
77
78
|
- lib/courrier/email/providers/cloudflare.rb
|
|
78
79
|
- lib/courrier/email/providers/lettermint.rb
|
|
79
80
|
- lib/courrier/email/providers/logger.rb
|
|
@@ -96,6 +97,7 @@ files:
|
|
|
96
97
|
- lib/courrier/subscriber.rb
|
|
97
98
|
- lib/courrier/subscriber/base.rb
|
|
98
99
|
- lib/courrier/subscriber/beehiiv.rb
|
|
100
|
+
- lib/courrier/subscriber/brevo.rb
|
|
99
101
|
- lib/courrier/subscriber/buttondown.rb
|
|
100
102
|
- lib/courrier/subscriber/kit.rb
|
|
101
103
|
- lib/courrier/subscriber/loops.rb
|
|
@@ -118,14 +120,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
118
120
|
requirements:
|
|
119
121
|
- - ">="
|
|
120
122
|
- !ruby/object:Gem::Version
|
|
121
|
-
version: 4.0
|
|
123
|
+
version: 3.4.0
|
|
122
124
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
125
|
requirements:
|
|
124
126
|
- - ">="
|
|
125
127
|
- !ruby/object:Gem::Version
|
|
126
128
|
version: '0'
|
|
127
129
|
requirements: []
|
|
128
|
-
rubygems_version:
|
|
130
|
+
rubygems_version: 3.6.7
|
|
129
131
|
specification_version: 4
|
|
130
132
|
summary: API-powered email delivery for Ruby apps
|
|
131
133
|
test_files: []
|