courrier 0.8.2 → 0.10.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: 3623ab587d2595e03eb10475ceab33725e643de6aa50fcd06641f6b0fb1686e0
4
- data.tar.gz: 8c650ed796a0a76fa2bca4fde2dde15854faad797b90fd0f7819e4dac2f0464b
3
+ metadata.gz: 555584763bab84eb75f07a5a7fa43fb4e4a65c3b1bb52de2c8e551c744fd0625
4
+ data.tar.gz: 4901dfab5c8c70d2f741e6c5edc0014e2aa7a9ca4afddc011d0d946464da709d
5
5
  SHA512:
6
- metadata.gz: ec2d11842243f85c194c69e5ca1abf8117e7026c84f62e46627a462512caa5f357bfa94eceb4a2856e11a9176b3702c609bd838798902146f115c1362aa4cf6c
7
- data.tar.gz: cacb341339ad98c2cb34abd0fe81f92a2a95891ba6814331e1c1caadc46f4929319fa11a6118cf622ee6f96b036e562ff76eba1fd1b5b9dec947c6db8ad4219f
6
+ metadata.gz: 93d5a592840d197493eafc3b8cac47b03218b9d8bdce88ef2e666685468a32879583ea7bb7adc1e84a665864ee5d4aebc80e982606f78836d2cf996e7fd5e602
7
+ data.tar.gz: d2e4391d1ea6d24031fbba9539f4935386b552184e145a4a2582e0a9a72003d3f79ed2afc6091ca9b2f4069b725e8a778602b8d31fe688999fad2badc4535276
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- courrier (0.8.2)
4
+ courrier (0.10.0)
5
5
  launchy (>= 3.1, < 4)
6
6
  nokogiri (>= 1.18, < 2)
7
7
 
data/README.md CHANGED
@@ -74,8 +74,6 @@ end
74
74
  # OrderEmail.deliver to: "recipient@railsdesigner.com"
75
75
  ```
76
76
 
77
- 💡 Write your email content using the [Minimal Email Editor](https://railsdesigner.com/minimal-email-editor/).
78
-
79
77
 
80
78
  ## Configuration
81
79
 
@@ -119,6 +117,23 @@ OrderEmail.deliver to: "recipient@railsdesigner.com",\
119
117
  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.
120
118
 
121
119
 
120
+ ## Custom headers
121
+
122
+ Email classes can define custom HTTP headers that are sent with every email:
123
+ ```ruby
124
+ class OrderEmail < Courrier::Email
125
+ headers list_unsubscribe_post: "List-Unsubscribe=One-Click"
126
+
127
+ def subject = "Rails Icons now supports SVG sprites!"
128
+
129
+ def text = # …
130
+ def markdown = # …
131
+ end
132
+ ```
133
+
134
+ Useful for adding provider-specific headers like List-Unsubscribe for Postmark, X-Mailer identifiers, or custom metadata headers required.
135
+
136
+
122
137
  ## Custom attributes
123
138
 
124
139
  Besides the standard email attributes (`from`, `to`, `reply_to`, etc.), you can pass any additional attributes that will be available in your email templates:
@@ -256,6 +271,86 @@ end
256
271
  ```
257
272
 
258
273
 
274
+ ### Template files
275
+
276
+ Instead of defining `text` and `html` methods, you can create ERB template files:
277
+ ```ruby
278
+ class OrderEmail < Courrier::Email
279
+ def subject = "Your order is ready!"
280
+ # text and html content will be loaded from template files
281
+ end
282
+ ```
283
+
284
+ Create template files alongside your email class:
285
+ - `app/emails/order_email.text.erb`
286
+ - `app/emails/order_email.html.erb`
287
+
288
+ Templates have access to all context options and instance variables:
289
+ ```erb
290
+ <!-- app/emails/order_email.html.erb -->
291
+ <h1>Hello <%= name %>!</h1>
292
+ <p>Your order #<%= order_id %> is ready for pickup.</p>
293
+ ```
294
+
295
+ Method definitions take precedence over template files when both exist. You can mix approaches. For example, define text in a method and use a template for the html:
296
+ ```ruby
297
+ class OrderEmail < Courrier::Email
298
+ def subject = "Your order is ready!"
299
+
300
+ def text = "Hello #{name}! Your order ##{order_id} is ready."
301
+
302
+ # html will be loaded from app/emails/order_email.html.erb
303
+ end
304
+ ```
305
+
306
+
307
+ ## Markdown support
308
+
309
+ 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.
310
+
311
+
312
+ ### Markdown methods
313
+
314
+ Define a `markdown` method in your email class:
315
+ ```ruby
316
+ class OrderEmail < Courrier::Email
317
+ def subject = "Your order is ready!"
318
+
319
+ def markdown
320
+ <<~MARKDOWN
321
+ # Hello #{name}!
322
+
323
+ Your order **##{order_id}** is ready for pickup.
324
+
325
+ ## Order Details
326
+ - Item: #{item_name}
327
+ - Price: #{price}
328
+ MARKDOWN
329
+ end
330
+ end
331
+ ```
332
+
333
+
334
+ ### Markdown templates
335
+
336
+ Create markdown template files alongside your email class:
337
+ - `app/emails/order_email.md.erb`
338
+ - `app/emails/order_email.markdown.erb`
339
+
340
+ ```erb
341
+ <!-- app/emails/order_email.md.erb -->
342
+ # Hello <%= name %>!
343
+
344
+ Your order **#<%= order_id %>** is ready for pickup.
345
+
346
+ ## Order Details
347
+ - Item: <%= item_name %>
348
+ - Price: <%= price %>
349
+ ```
350
+
351
+ 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.
352
+
353
+
259
354
  ### Auto-generate text from HTML
260
355
 
261
356
  Automatically generate plain text versions from your HTML emails:
@@ -414,10 +509,7 @@ Yes! While different in approach, Courrier can fully replace ActionMailer. It's
414
509
  Not at all! While Courrier has some Rails-specific goodies (like the inbox preview feature and generators), it works great with any Ruby application.
415
510
 
416
511
  ### Can it send using SMTP?
417
- No - Courrier is specifically built for API-based email delivery. If SMTP is needed, ActionMailer would be a better choices.
418
-
419
- ### Can separate view templates be created (like ActionMailer)?
420
- The approach is different here. Instead of separate view files, email content is defined right in the email class using `text` and `html` methods. Layouts can be used to share common templates. This makes emails more self-contained and easier to reason about.
512
+ No. Courrier is specifically built for API-based email delivery. If SMTP is needed, ActionMailer would be a better choices.
421
513
 
422
514
  ### What's the main benefit over ActionMailer?
423
515
  Courrier offers a simpler, more modern approach to sending emails. Each email is a standalone class, configuration is straightforward (typically just only an API key is needed) and it packs few quality-of-life features (like the inbox feature and auto-generate text version).
@@ -30,31 +30,37 @@ module Courrier
30
30
  userlist: Courrier::Email::Providers::Userlist
31
31
  }
32
32
 
33
- def initialize(provider: nil, api_key: nil, options: {}, provider_options: {}, context_options: {})
33
+ def initialize(provider: nil, api_key: nil, options: {}, provider_options: {}, context_options: {}, custom_headers: {})
34
34
  @provider = provider
35
35
  @api_key = api_key
36
36
 
37
37
  @options = options
38
38
  @provider_options = provider_options
39
39
  @context_options = context_options
40
+ @custom_headers = custom_headers
40
41
  end
41
42
 
42
43
  def deliver
43
- raise Courrier::ConfigurationError, "`provider` and `api_key` must be configured for production environment" if configuration_missing_in_production?
44
- raise Courrier::ConfigurationError, "Unknown provider. Choose one of `#{comma_separated_providers}` or provide your own." if @provider.nil? || @provider.to_s.strip.empty?
44
+ raise Courrier::ConfigurationError, "Unknown provider. Choose one of `#{comma_separated_providers}` or provide your own." if provider_invalid?
45
+ raise Courrier::ConfigurationError, "API key must be configured for #{@provider} provider in production environment" if configuration_missing_in_production?
45
46
 
46
47
  provider_class.new(
47
48
  api_key: @api_key,
48
49
  options: @options,
49
50
  provider_options: @provider_options,
50
- context_options: @context_options
51
+ context_options: @context_options,
52
+ custom_headers: @custom_headers
51
53
  ).deliver
52
54
  end
53
55
 
54
56
  private
55
57
 
58
+ def provider_invalid?
59
+ @provider.nil? || @provider.to_s.strip.empty?
60
+ end
61
+
56
62
  def configuration_missing_in_production?
57
- production? && required_attributes_blank?
63
+ production? && api_key_required_providers? && api_key_blank?
58
64
  end
59
65
 
60
66
  def comma_separated_providers = PROVIDERS.keys.join(", ")
@@ -65,7 +71,13 @@ module Courrier
65
71
  Object.const_get(@provider)
66
72
  end
67
73
 
68
- def required_attributes_blank? = @api_key.empty?
74
+ def api_key_required_providers?
75
+ !%w[logger inbox].include?(@provider.to_s)
76
+ end
77
+
78
+ def api_key_blank?
79
+ @api_key.nil? || @api_key.to_s.strip.empty?
80
+ end
69
81
 
70
82
  def production?
71
83
  defined?(Rails) && Rails.env.production?
@@ -6,11 +6,12 @@ module Courrier
6
6
  class Email
7
7
  module Providers
8
8
  class Base
9
- def initialize(api_key: nil, options: {}, provider_options: {}, context_options: {})
9
+ def initialize(api_key: nil, options: {}, provider_options: {}, context_options: {}, custom_headers: {})
10
10
  @api_key = api_key
11
11
  @options = options
12
12
  @provider_options = provider_options
13
13
  @context_options = context_options
14
+ @custom_headers = custom_headers
14
15
  end
15
16
 
16
17
  def deliver
@@ -31,7 +32,11 @@ module Courrier
31
32
 
32
33
  def content_type = "application/json"
33
34
 
34
- def headers = {}
35
+ def headers
36
+ default_headers.merge(@custom_headers)
37
+ end
38
+
39
+ def default_headers = {}
35
40
 
36
41
  def provider = self.class.name.split("::").last
37
42
  end
@@ -16,7 +16,7 @@ module Courrier
16
16
 
17
17
  private
18
18
 
19
- def headers
19
+ def default_headers
20
20
  {
21
21
  "Authorization" => "Bearer #{@api_key}"
22
22
  }
@@ -29,7 +29,7 @@ module Courrier
29
29
 
30
30
  def content_type = "multipart/form-data"
31
31
 
32
- def headers
32
+ def default_headers
33
33
  {
34
34
  "Authorization" => "Basic #{Base64.strict_encode64("api:#{@api_key}")}"
35
35
  }
@@ -31,7 +31,7 @@ module Courrier
31
31
 
32
32
  private
33
33
 
34
- def headers
34
+ def default_headers
35
35
  {
36
36
  "Authorization" => "Basic " + Base64.strict_encode64("#{@api_key}:#{@provider_options.api_secret}")
37
37
  }
@@ -21,7 +21,7 @@ module Courrier
21
21
 
22
22
  private
23
23
 
24
- def headers
24
+ def default_headers
25
25
  {
26
26
  "MailPace-Server-Token" => @api_key
27
27
  }
@@ -23,7 +23,7 @@ module Courrier
23
23
 
24
24
  private
25
25
 
26
- def headers
26
+ def default_headers
27
27
  {
28
28
  "X-Postmark-Server-Token" => @api_key
29
29
  }
@@ -21,7 +21,7 @@ module Courrier
21
21
 
22
22
  private
23
23
 
24
- def headers
24
+ def default_headers
25
25
  {
26
26
  "Authorization" => "Bearer #{@api_key}"
27
27
  }
@@ -38,7 +38,7 @@ module Courrier
38
38
 
39
39
  private
40
40
 
41
- def headers
41
+ def default_headers
42
42
  {
43
43
  "Authorization" => "Bearer #{@api_key}"
44
44
  }
@@ -27,7 +27,7 @@ module Courrier
27
27
 
28
28
  private
29
29
 
30
- def headers
30
+ def default_headers
31
31
  {
32
32
  "Authorization" => @api_key
33
33
  }
@@ -17,7 +17,7 @@ module Courrier
17
17
 
18
18
  private
19
19
 
20
- def headers
20
+ def default_headers
21
21
  {
22
22
  "Authorization" => "Push #{@api_key}"
23
23
  }
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "erb"
4
+
3
5
  require "courrier/email/address"
4
6
  require "courrier/jobs/email_delivery_job" if defined?(Rails)
5
7
  require "courrier/email/layouts"
8
+ require "courrier/markdown"
6
9
  require "courrier/email/options"
7
10
  require "courrier/email/provider"
8
11
 
@@ -34,7 +37,11 @@ module Courrier
34
37
  @queue_options ||= {}
35
38
  end
36
39
 
37
- attr_writer :queue_options
40
+ attr_writer :queue_options, :headers
41
+
42
+ def headers(**options)
43
+ options.empty? ? (@headers ||= {}) : @headers = options
44
+ end
38
45
 
39
46
  def enqueue(**options)
40
47
  self.queue_options = options
@@ -98,7 +105,8 @@ module Courrier
98
105
  api_key: @api_key,
99
106
  options: @options,
100
107
  provider_options: Courrier.configuration&.providers&.[](@provider.to_s.downcase.to_sym),
101
- context_options: @context_options
108
+ context_options: @context_options,
109
+ custom_headers: self.class.headers
102
110
  ).deliver
103
111
  end
104
112
  alias_method :deliver_now, :deliver
@@ -133,7 +141,44 @@ module Courrier
133
141
  ENV["COURRIER_EMAIL_DISABLED"] == "true" || ENV["COURRIER_EMAIL_ENABLED"] == "false"
134
142
  end
135
143
 
136
- def method_missing(name, *) = @context_options[name]
144
+ def method_missing(name, *)
145
+ if name == :text || name == :html
146
+ render_template(name.to_s).tap do |result|
147
+ return result || markdown_rendered if name == :html
148
+ end
149
+ else
150
+ @context_options[name]
151
+ end
152
+ end
153
+
154
+ def render_template(format)
155
+ template_path = template_file_path(format)
156
+
157
+ File.exist?(template_path) ? ERB.new(File.read(template_path)).result(binding) : nil
158
+ end
159
+
160
+ def render_markdown_template
161
+ %w[md markdown].each do |ext|
162
+ template_path = template_file_path(ext)
163
+
164
+ return ERB.new(File.read(template_path)).result(binding) if File.exist?(template_path)
165
+ end
166
+
167
+ nil
168
+ end
169
+
170
+ def markdown_rendered
171
+ return unless Courrier::Markdown.available?
172
+
173
+ markdown_content = render_markdown_template || (respond_to?(:markdown, true) ? markdown : nil)
174
+ Courrier::Markdown.render(markdown_content) if markdown_content
175
+ end
176
+
177
+ def template_file_path(format)
178
+ class_path = self.class.name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
179
+
180
+ File.join(Courrier.configuration&.email_path, "#{class_path}.#{format}.erb")
181
+ end
137
182
 
138
183
  def respond_to_missing?(name, include_private = false) = true
139
184
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Courrier
4
+ class Markdown
5
+ class << self
6
+ def available?
7
+ defined?(::Redcarpet) || defined?(::Kramdown) || defined?(::Commonmarker)
8
+ end
9
+
10
+ def render(text)
11
+ return unless available?
12
+
13
+ parser.parse(text.to_s)
14
+ end
15
+
16
+ private
17
+
18
+ def parser
19
+ @parser ||= available_parser.new
20
+ end
21
+
22
+ def available_parser
23
+ return RedcarpetParser if defined?(::Redcarpet)
24
+ return KramdownParser if defined?(::Kramdown)
25
+ return CommonmarkerParser if defined?(::Commonmarker)
26
+
27
+ Parser
28
+ end
29
+ end
30
+
31
+ class Parser
32
+ def parse(text) = text.to_s
33
+ end
34
+
35
+ class RedcarpetParser < Parser
36
+ def parse(text)
37
+ renderer = Redcarpet::Render::HTML.new
38
+ markdown = Redcarpet::Markdown.new(renderer)
39
+
40
+ markdown.render(text)
41
+ end
42
+ end
43
+
44
+ class KramdownParser < Parser
45
+ def parse(text) = Kramdown::Document.new(text).to_html
46
+ end
47
+
48
+ class CommonmarkerParser < Parser
49
+ def parse(text) = Commonmarker.to_html(text)
50
+ end
51
+ end
52
+ end
@@ -1,3 +1,3 @@
1
1
  module Courrier
2
- VERSION = "0.8.2"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -1,8 +1,6 @@
1
1
  class <%= class_name %><%= options[:skip_suffix] ? "" : "Email" %> < <%= parent_class %>
2
2
  def subject = ""
3
3
 
4
- # Create HTML and text emails using:
5
- # https://railsdesigner.com/minimal-email-editor/
6
4
  def text
7
5
  <<~TEXT
8
6
  TEXT
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: courrier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-20 00:00:00.000000000 Z
11
+ date: 2026-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: launchy
@@ -98,6 +98,7 @@ files:
98
98
  - lib/courrier/engine.rb
99
99
  - lib/courrier/errors.rb
100
100
  - lib/courrier/jobs/email_delivery_job.rb
101
+ - lib/courrier/markdown.rb
101
102
  - lib/courrier/railtie.rb
102
103
  - lib/courrier/subscriber.rb
103
104
  - lib/courrier/subscriber/base.rb
@@ -137,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
138
  - !ruby/object:Gem::Version
138
139
  version: '0'
139
140
  requirements: []
140
- rubygems_version: 3.4.1
141
+ rubygems_version: 3.4.10
141
142
  signing_key:
142
143
  specification_version: 4
143
144
  summary: API-powered email delivery for Ruby apps