courrier 0.9.0 → 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: 8a3d93b4337d15e066ffa7ed4626dd254231ee744940de07b7b76be8714afbde
4
- data.tar.gz: d7ef7a3ed1a8600581b6bdf13f63072c49ad138ad52384e1c358dbcd8f8a45b2
3
+ metadata.gz: 555584763bab84eb75f07a5a7fa43fb4e4a65c3b1bb52de2c8e551c744fd0625
4
+ data.tar.gz: 4901dfab5c8c70d2f741e6c5edc0014e2aa7a9ca4afddc011d0d946464da709d
5
5
  SHA512:
6
- metadata.gz: 32d2fba728012eb703ca67044484a2f0caf0177ee0f341b6973566a50136f6531dfc37edfe7347511b4de67291af0364f6f03ad4c22247f5bbd72ca306e13455
7
- data.tar.gz: 5a3ea2ea5fb0637f025a3577d6e1004a66478e0929b96752b0fdea302af95f6c88af3bed801265dc14f6645a8014adb781cd1cc04ee1809bedf76ca892f4c4f0
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.9.0)
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:
@@ -289,6 +304,53 @@ end
289
304
  ```
290
305
 
291
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
+
292
354
  ### Auto-generate text from HTML
293
355
 
294
356
  Automatically generate plain text versions from your HTML emails:
@@ -447,10 +509,7 @@ Yes! While different in approach, Courrier can fully replace ActionMailer. It's
447
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.
448
510
 
449
511
  ### Can it send using SMTP?
450
- No - Courrier is specifically built for API-based email delivery. If SMTP is needed, ActionMailer would be a better choices.
451
-
452
- ### Can separate view templates be created (like ActionMailer)?
453
- 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.
454
513
 
455
514
  ### What's the main benefit over ActionMailer?
456
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,13 +30,14 @@ 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
@@ -47,7 +48,8 @@ module Courrier
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
 
@@ -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
  }
@@ -5,6 +5,7 @@ require "erb"
5
5
  require "courrier/email/address"
6
6
  require "courrier/jobs/email_delivery_job" if defined?(Rails)
7
7
  require "courrier/email/layouts"
8
+ require "courrier/markdown"
8
9
  require "courrier/email/options"
9
10
  require "courrier/email/provider"
10
11
 
@@ -36,7 +37,11 @@ module Courrier
36
37
  @queue_options ||= {}
37
38
  end
38
39
 
39
- attr_writer :queue_options
40
+ attr_writer :queue_options, :headers
41
+
42
+ def headers(**options)
43
+ options.empty? ? (@headers ||= {}) : @headers = options
44
+ end
40
45
 
41
46
  def enqueue(**options)
42
47
  self.queue_options = options
@@ -100,7 +105,8 @@ module Courrier
100
105
  api_key: @api_key,
101
106
  options: @options,
102
107
  provider_options: Courrier.configuration&.providers&.[](@provider.to_s.downcase.to_sym),
103
- context_options: @context_options
108
+ context_options: @context_options,
109
+ custom_headers: self.class.headers
104
110
  ).deliver
105
111
  end
106
112
  alias_method :deliver_now, :deliver
@@ -137,7 +143,9 @@ module Courrier
137
143
 
138
144
  def method_missing(name, *)
139
145
  if name == :text || name == :html
140
- render_template(name.to_s)
146
+ render_template(name.to_s).tap do |result|
147
+ return result || markdown_rendered if name == :html
148
+ end
141
149
  else
142
150
  @context_options[name]
143
151
  end
@@ -149,6 +157,23 @@ module Courrier
149
157
  File.exist?(template_path) ? ERB.new(File.read(template_path)).result(binding) : nil
150
158
  end
151
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
+
152
177
  def template_file_path(format)
153
178
  class_path = self.class.name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
154
179
 
@@ -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.9.0"
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.9.0
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-02-04 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