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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +98 -6
- data/lib/courrier/email/provider.rb +18 -6
- data/lib/courrier/email/providers/base.rb +7 -2
- data/lib/courrier/email/providers/loops.rb +1 -1
- data/lib/courrier/email/providers/mailgun.rb +1 -1
- data/lib/courrier/email/providers/mailjet.rb +1 -1
- data/lib/courrier/email/providers/mailpace.rb +1 -1
- data/lib/courrier/email/providers/postmark.rb +1 -1
- data/lib/courrier/email/providers/resend.rb +1 -1
- data/lib/courrier/email/providers/sendgrid.rb +1 -1
- data/lib/courrier/email/providers/sparkpost.rb +1 -1
- data/lib/courrier/email/providers/userlist.rb +1 -1
- data/lib/courrier/email.rb +48 -3
- data/lib/courrier/markdown.rb +52 -0
- data/lib/courrier/version.rb +1 -1
- data/lib/generators/courrier/templates/email.rb.tt +0 -2
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 555584763bab84eb75f07a5a7fa43fb4e4a65c3b1bb52de2c8e551c744fd0625
|
|
4
|
+
data.tar.gz: 4901dfab5c8c70d2f741e6c5edc0014e2aa7a9ca4afddc011d0d946464da709d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93d5a592840d197493eafc3b8cac47b03218b9d8bdce88ef2e666685468a32879583ea7bb7adc1e84a665864ee5d4aebc80e982606f78836d2cf996e7fd5e602
|
|
7
|
+
data.tar.gz: d2e4391d1ea6d24031fbba9539f4935386b552184e145a4a2582e0a9a72003d3f79ed2afc6091ca9b2f4069b725e8a778602b8d31fe688999fad2badc4535276
|
data/Gemfile.lock
CHANGED
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
|
|
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, "
|
|
44
|
-
raise Courrier::ConfigurationError, "
|
|
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? &&
|
|
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
|
|
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
|
data/lib/courrier/email.rb
CHANGED
|
@@ -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, *)
|
|
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
|
data/lib/courrier/version.rb
CHANGED
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.
|
|
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-
|
|
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.
|
|
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
|