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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d793cbc73ad9612cbb5e010e6aed85f9885e41fc8b672d16f82a6d155f63b726
4
- data.tar.gz: 1143fc17abc330c3f94192ec95aea7618583266491f109b62aee3178826ede97
3
+ metadata.gz: b0a97b8804b294b065433077d025cde44716b87821214649a6421cac278beca5
4
+ data.tar.gz: 814110bf00d835bfacb15bc8f59bdeb04a0cb7f7b572d4418c3fd901e98c1115
5
5
  SHA512:
6
- metadata.gz: 64991e00fdb46c81164c29ffc7dc9de6078bcbc6cad12407355424f19f54e3a767e8732abf1438105d1f14b3ebe8ecf8367aa88bd7e53fadd7787fa8dd163609
7
- data.tar.gz: eed7664f6a7211fc98e8cd53e0dc24ad1784e5373575c006feb04fb7d3a2706da3127c0db801b3c960697f774cd3df68df3622b975f365ed5fdfa211327aa3d3
6
+ metadata.gz: fc4503ec4cef48dbd12f67f093ccb3df9b29834d34b26f99af90b8020f9be2f7207b7629be79d50ba90af9b7b01c47e104568c1813b7b5296211ed788f453831
7
+ data.tar.gz: d50436d4e7417f481f3f2ad2cc59962b30f42fd60d471f7226128e02e04c0378c117968a8c64c8346a735af2e1dae843d1d0447bdb9ce91153850d9e67af1d83
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- courrier (0.11.0)
4
+ courrier (1.0.0)
5
5
  logger (>= 1.5, < 3)
6
6
  nokogiri (>= 1.18, < 2)
7
7
 
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 HTTP headers that are sent with every email:
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 help with development and testing:
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 content to HTML when a markdown gem is available. Simply bundle any supported markdown gem (`redcarpet`, `kramdown` or `commonmarker`) and it will be used.
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.provider = "logger" # outputs emails to STDOUT
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.provider = "CustomProvider"
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
- Manage subscribers across popular email marketing platforms:
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.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,
@@ -6,6 +6,8 @@ module Courrier
6
6
  class Email
7
7
  module Providers
8
8
  class Base
9
+ def self.config_options = []
10
+
9
11
  def initialize(api_key: nil, options: {}, provider_options: {}, context_options: {}, custom_headers: {})
10
12
  @api_key = api_key
11
13
  @options = options
@@ -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 Cloudflare < Base
7
+ def self.config_options = %w[account_id]
8
+
7
9
  def body
8
10
  {
9
11
  "from" => @options.from,
@@ -4,6 +4,8 @@ module Courrier
4
4
  class Email
5
5
  module Providers
6
6
  class Lettermint < Base
7
+ def self.config_options = %w[route]
8
+
7
9
  ENDPOINT_URL = "https://api.lettermint.co/v1/send"
8
10
 
9
11
  def body
@@ -4,6 +4,8 @@ module Courrier
4
4
  class Email
5
5
  module Providers
6
6
  class Loops < Base
7
+ def self.config_options = %w[transactional_id]
8
+
7
9
  ENDPOINT_URL = "https://app.loops.so/api/v1/transactional"
8
10
 
9
11
  def body
@@ -4,6 +4,8 @@ module Courrier
4
4
  class Email
5
5
  module Providers
6
6
  class Mailgun < Base
7
+ def self.config_options = %w[domain]
8
+
7
9
  ENDPOINT_URL = "https://api.mailgun.net/v3/%{domain}/messages"
8
10
 
9
11
  def body
@@ -4,6 +4,8 @@ module Courrier
4
4
  class Email
5
5
  module Providers
6
6
  class Mailjet < Base
7
+ def self.config_options = %w[api_secret]
8
+
7
9
  ENDPOINT_URL = "https://api.mailjet.com/v3.1/send"
8
10
 
9
11
  def body
@@ -4,6 +4,8 @@ module Courrier
4
4
  class Email
5
5
  module Providers
6
6
  class Postmark < Base
7
+ def self.config_options = %w[message_stream]
8
+
7
9
  ENDPOINT_URL = "https://api.postmarkapp.com/email"
8
10
 
9
11
  def body
@@ -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
 
@@ -4,6 +4,8 @@ module Courrier
4
4
  class Email
5
5
  module Providers
6
6
  class Userlist < Base
7
+ def self.config_options = %w[theme]
8
+
7
9
  ENDPOINT_URL = "https://push.userlist.com/messages"
8
10
 
9
11
  def body
@@ -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] || ENV["COURRIER_PROVIDER"] || self.class.provider || Courrier.configuration&.email&.[](:provider)
82
- @api_key = options[:api_key] || ENV["COURRIER_API_KEY"] || self.class.api_key || Courrier.configuration&.email&.[](: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
@@ -1,3 +1,3 @@
1
1
  module Courrier
2
- VERSION = "0.11.0"
2
+ VERSION = "1.0.0"
3
3
  end
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.11.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.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: 4.0.3
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: []