courrier 0.7.0 → 0.8.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 +142 -20
- data/lib/courrier/configuration.rb +29 -3
- data/lib/courrier/email/options.rb +15 -0
- data/lib/courrier/email/provider.rb +16 -15
- data/lib/courrier/email/providers/inbox.rb +5 -1
- data/lib/courrier/email/providers/logger.rb +22 -3
- data/lib/courrier/email/providers/userlist.rb +13 -13
- data/lib/courrier/email.rb +52 -10
- data/lib/courrier/errors.rb +3 -1
- data/lib/courrier/jobs/email_delivery_job.rb +23 -0
- data/lib/courrier/subscriber/base.rb +51 -0
- data/lib/courrier/subscriber/beehiiv.rb +45 -0
- data/lib/courrier/subscriber/buttondown.rb +28 -0
- data/lib/courrier/subscriber/kit.rb +36 -0
- data/lib/courrier/subscriber/loops.rb +32 -0
- data/lib/courrier/subscriber/mailchimp.rb +39 -0
- data/lib/courrier/subscriber/mailerlite.rb +28 -0
- data/lib/courrier/subscriber/result.rb +41 -0
- data/lib/courrier/subscriber.rb +34 -0
- data/lib/courrier/version.rb +1 -1
- data/lib/courrier.rb +1 -0
- data/lib/generators/courrier/email_generator.rb +24 -1
- data/lib/generators/courrier/templates/email/password_reset.rb.tt +29 -0
- data/lib/generators/courrier/templates/email/welcome.rb.tt +43 -0
- data/lib/generators/courrier/templates/initializer.rb.tt +10 -5
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e43b1cb6985e742ca84f68be14ddf25e72bf8f64a26ce7be151272983996ac45
|
|
4
|
+
data.tar.gz: ac7b82db6e5fd9975a76de6a630a4ac8e3f956595e175fc6d6c1ef18d9d4949c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f5ad1ecb424cca190a86bd10acbf69c906e3f7218724a3cdc29e8ce3e10c4bea4c1c43ba66d6de24af17f53c9db416c659e7b84aadd92fcece63b57ee5a13ce
|
|
7
|
+
data.tar.gz: 9050571fcc8050ecb6a92a321e466f92951c383d3ec1376809f70913440f38537f8245b37f2955bca3c263874d9de178818789dae89cc2a7bfe809c3d9d39eb9
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
# Courrier
|
|
2
2
|
|
|
3
|
-
API-powered email delivery for Ruby apps
|
|
3
|
+
API-powered email delivery and newsletter subscription management for Ruby apps
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
# Quick example
|
|
9
|
+
class OrderEmail < Courrier::Email
|
|
10
|
+
def subject = "Here is your order!"
|
|
11
|
+
|
|
12
|
+
def text = "Thanks for ordering"
|
|
13
|
+
|
|
14
|
+
def html = "<p>Thanks for ordering</p>"
|
|
15
|
+
end
|
|
16
|
+
|
|
9
17
|
OrderEmail.deliver to: "recipient@railsdesigner.com"
|
|
18
|
+
|
|
19
|
+
# Manage newsletter subscriptions
|
|
20
|
+
Courrier::Subscriber.create "subscriber@example.com"
|
|
10
21
|
```
|
|
11
22
|
|
|
12
23
|
<a href="https://railsdesigner.com/" target="_blank">
|
|
@@ -72,8 +83,11 @@ Courrier uses a configuration system with three levels (from lowest to highest p
|
|
|
72
83
|
1. **Global configuration**
|
|
73
84
|
```ruby
|
|
74
85
|
Courrier.configure do |config|
|
|
75
|
-
config.
|
|
76
|
-
|
|
86
|
+
config.email = {
|
|
87
|
+
provider: "postmark",
|
|
88
|
+
api_key: "xyz"
|
|
89
|
+
}
|
|
90
|
+
|
|
77
91
|
config.from = "devs@railsdesigner.com"
|
|
78
92
|
config.default_url_options = { host: "railsdesigner.com" }
|
|
79
93
|
|
|
@@ -88,7 +102,7 @@ end
|
|
|
88
102
|
class OrderEmail < Courrier::Email
|
|
89
103
|
configure from: "orders@railsdesigner.com",
|
|
90
104
|
cc: "records@railsdesigner.com",
|
|
91
|
-
provider: "mailgun"
|
|
105
|
+
provider: "mailgun"
|
|
92
106
|
end
|
|
93
107
|
```
|
|
94
108
|
|
|
@@ -104,7 +118,7 @@ OrderEmail.deliver to: "recipient@railsdesigner.com",\
|
|
|
104
118
|
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.
|
|
105
119
|
|
|
106
120
|
|
|
107
|
-
## Custom
|
|
121
|
+
## Custom attributes
|
|
108
122
|
|
|
109
123
|
Besides the standard email attributes (`from`, `to`, `reply_to`, etc.), you can pass any additional attributes that will be available in your email templates:
|
|
110
124
|
```ruby
|
|
@@ -122,12 +136,12 @@ end
|
|
|
122
136
|
```
|
|
123
137
|
|
|
124
138
|
|
|
125
|
-
## Result
|
|
139
|
+
## Result object
|
|
126
140
|
|
|
127
141
|
When sending an email through Courrier, a `Result` object is returned that provides information about the delivery attempt. This object offers a simple interface to check the status and access response data.
|
|
128
142
|
|
|
129
143
|
|
|
130
|
-
### Available
|
|
144
|
+
### Available methods
|
|
131
145
|
|
|
132
146
|
| Method | Return Type | Description |
|
|
133
147
|
|:-------|:-----------|:------------|
|
|
@@ -157,22 +171,28 @@ Courrier supports these transactional email providers:
|
|
|
157
171
|
|
|
158
172
|
- [Loops](https://loops.so)
|
|
159
173
|
- [Mailgun](https://mailgun.com)
|
|
160
|
-
- [Mailjet](https://mailjet.com)
|
|
161
174
|
- [MailPace](https://mailpace.com)
|
|
162
175
|
- [Postmark](https://postmarkapp.com)
|
|
163
176
|
- [Resend](https://resend.com)
|
|
164
|
-
- [SendGrid](https://sendgrid.com)
|
|
165
|
-
- [SparkPost](https://sparkpost.com)
|
|
166
177
|
- [Userlist](https://userlist.com)
|
|
167
178
|
|
|
168
|
-
⚠️ Some providers still need manual verification of their implementation. If you're using one of these providers, please help verify the implementation by sharing your experience in [this GitHub issue](https://github.com/Rails-Designer/courrier/issues/4). 🙏
|
|
169
|
-
|
|
170
179
|
|
|
171
180
|
## More Features
|
|
172
181
|
|
|
173
182
|
Additional functionality to help with development and testing:
|
|
174
183
|
|
|
175
184
|
|
|
185
|
+
### Background jobs (Rails only)
|
|
186
|
+
|
|
187
|
+
Use `deliver_later` to enqueue delivering using Rails' ActiveJob. You can set
|
|
188
|
+
various ActiveJob-supported options in the email class, like so: `enqueue queue: "emails", wait: 5.minutes`.
|
|
189
|
+
|
|
190
|
+
- `queue`, enqueue the email on the specified queue;
|
|
191
|
+
- `wait`, enqueue the email to be delivered with a delay;
|
|
192
|
+
- `wait_until`, enqueue the email to be delivered at (after) a specific date/time;
|
|
193
|
+
- `priority`, enqueues the email with the specified priority.
|
|
194
|
+
|
|
195
|
+
|
|
176
196
|
### Inbox (Rails only)
|
|
177
197
|
|
|
178
198
|
You can preview your emails in the inbox:
|
|
@@ -192,7 +212,7 @@ config.inbox.auto_open = true
|
|
|
192
212
|
Emails are automatically cleared with `bin/rails tmp:clear`, or manually with `bin/rails courrier:clear`.
|
|
193
213
|
|
|
194
214
|
|
|
195
|
-
### Layout
|
|
215
|
+
### Layout support
|
|
196
216
|
|
|
197
217
|
Wrap your email content using layouts:
|
|
198
218
|
```ruby
|
|
@@ -235,7 +255,7 @@ end
|
|
|
235
255
|
```
|
|
236
256
|
|
|
237
257
|
|
|
238
|
-
### Auto-generate
|
|
258
|
+
### Auto-generate text from HTML
|
|
239
259
|
|
|
240
260
|
Automatically generate plain text versions from your HTML emails:
|
|
241
261
|
```ruby
|
|
@@ -243,7 +263,7 @@ config.auto_generate_text = true # Defaults to false
|
|
|
243
263
|
```
|
|
244
264
|
|
|
245
265
|
|
|
246
|
-
### Email
|
|
266
|
+
### Email address helper
|
|
247
267
|
|
|
248
268
|
Compose email addresses with display names:
|
|
249
269
|
```ruby
|
|
@@ -270,16 +290,17 @@ end
|
|
|
270
290
|
```
|
|
271
291
|
|
|
272
292
|
|
|
273
|
-
### Logger
|
|
293
|
+
### Logger provider
|
|
274
294
|
|
|
275
295
|
Use Ruby's built-in Logger for development and testing:
|
|
276
296
|
|
|
277
297
|
```ruby
|
|
278
|
-
config.provider = "logger" #
|
|
279
|
-
config.logger = custom_logger #
|
|
298
|
+
config.provider = "logger" # outputs emails to STDOUT
|
|
299
|
+
config.logger = custom_logger # optional: defaults to ::Logger.new($stdout)
|
|
280
300
|
```
|
|
281
301
|
|
|
282
|
-
|
|
302
|
+
|
|
303
|
+
### Custom providers
|
|
283
304
|
|
|
284
305
|
Create your own provider by inheriting from `Courrier::Email::Providers::Base`:
|
|
285
306
|
```ruby
|
|
@@ -300,6 +321,107 @@ config.provider = "CustomProvider"
|
|
|
300
321
|
Check the [existing providers](https://github.com/Rails-Designer/courrier/tree/main/lib/courrier/email/providers) for implementation examples.
|
|
301
322
|
|
|
302
323
|
|
|
324
|
+
## Newsletter subscriptions
|
|
325
|
+
|
|
326
|
+
Manage subscribers across popular email marketing platforms:
|
|
327
|
+
```ruby
|
|
328
|
+
Courrier.configure do |config|
|
|
329
|
+
config.subscriber = {
|
|
330
|
+
provider: "buttondown",
|
|
331
|
+
api_key: "your_api_key"
|
|
332
|
+
}
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
# Add a subscriber
|
|
338
|
+
subscriber = Courrier::Subscriber.create "subscriber@example.com"
|
|
339
|
+
|
|
340
|
+
# Remove a subscriber
|
|
341
|
+
subscriber = Courrier::Subscriber.destroy "subscriber@example.com"
|
|
342
|
+
|
|
343
|
+
if subscriber.success?
|
|
344
|
+
puts "Subscriber added!"
|
|
345
|
+
else
|
|
346
|
+
puts "Error: #{subscriber.error}"
|
|
347
|
+
end
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
### Supported providers
|
|
352
|
+
|
|
353
|
+
- [Beehiiv](https://www.beehiiv.com/) - requires `publication_id`
|
|
354
|
+
- [Buttondown](https://buttondown.com)
|
|
355
|
+
- [Kit](https://kit.com/) (formerly ConvertKit) - requires `form_id`
|
|
356
|
+
- [Loops](https://loops.so/)
|
|
357
|
+
- [Mailchimp](https://mailchimp.com/) - requires `dc` and `list_id`
|
|
358
|
+
- [MailerLite](https://www.mailerlite.com/)
|
|
359
|
+
|
|
360
|
+
Provider-specific configuration:
|
|
361
|
+
```ruby
|
|
362
|
+
config.subscriber = {
|
|
363
|
+
provider: "mailchimp",
|
|
364
|
+
api_key: "your_api_key",
|
|
365
|
+
dc: "us19",
|
|
366
|
+
list_id: "abc123"
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Custom providers
|
|
371
|
+
|
|
372
|
+
Create custom providers by inheriting from `Courrier::Subscriber::Base`:
|
|
373
|
+
```ruby
|
|
374
|
+
class CustomSubscriberProvider < Courrier::Subscriber::Base
|
|
375
|
+
ENDPOINT_URL = "https://api.example.com/subscribers"
|
|
376
|
+
|
|
377
|
+
def create(email)
|
|
378
|
+
request(:post, ENDPOINT_URL, {"email" => email})
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def destroy(email)
|
|
382
|
+
request(:delete, "#{ENDPOINT_URL}/#{email}")
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
private
|
|
386
|
+
|
|
387
|
+
def headers
|
|
388
|
+
{
|
|
389
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
390
|
+
"Content-Type" => "application/json"
|
|
391
|
+
}
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Then configure it:
|
|
397
|
+
```ruby
|
|
398
|
+
config.subscriber = {
|
|
399
|
+
provider: CustomSubscriberProvider,
|
|
400
|
+
api_key: "your_api_key"
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
See [existing providers](https://github.com/Rails-Designer/courrier/tree/main/lib/courrier/subscriber) for more examples.
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
## FAQ
|
|
408
|
+
|
|
409
|
+
### Is this a replacement for ActionMailer?
|
|
410
|
+
Yes! While different in approach, Courrier can fully replace ActionMailer. It's a modern alternative that focuses on API-based delivery. The main difference is in how emails are structured - Courrier uses a more straightforward, class-based approach.
|
|
411
|
+
|
|
412
|
+
### Is this for Rails only?
|
|
413
|
+
Not at all! While Courrier has some Rails-specific goodies (like the inbox preview feature and generators), it works great with any Ruby application.
|
|
414
|
+
|
|
415
|
+
### Can it send using SMTP?
|
|
416
|
+
No - Courrier is specifically built for API-based email delivery. If SMTP is needed, ActionMailer would be a better choices.
|
|
417
|
+
|
|
418
|
+
### Can separate view templates be created (like ActionMailer)?
|
|
419
|
+
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.
|
|
420
|
+
|
|
421
|
+
### What's the main benefit over ActionMailer?
|
|
422
|
+
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).
|
|
423
|
+
|
|
424
|
+
|
|
303
425
|
## Contributing
|
|
304
426
|
|
|
305
427
|
This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `rake` before submitting pull requests.
|
|
@@ -19,13 +19,15 @@ module Courrier
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
class Configuration
|
|
22
|
-
attr_accessor :
|
|
22
|
+
attr_accessor :email, :subscriber, :logger, :email_path, :layouts, :default_url_options, :auto_generate_text,
|
|
23
23
|
:from, :reply_to, :cc, :bcc
|
|
24
|
+
|
|
24
25
|
attr_reader :providers, :inbox
|
|
25
26
|
|
|
26
27
|
def initialize
|
|
27
|
-
@
|
|
28
|
-
@
|
|
28
|
+
@email = {provider: "logger"}
|
|
29
|
+
@subscriber = {}
|
|
30
|
+
|
|
29
31
|
@logger = ::Logger.new($stdout)
|
|
30
32
|
@email_path = default_email_path
|
|
31
33
|
|
|
@@ -42,6 +44,30 @@ module Courrier
|
|
|
42
44
|
@inbox = Courrier::Configuration::Inbox.new
|
|
43
45
|
end
|
|
44
46
|
|
|
47
|
+
def provider
|
|
48
|
+
warn "[DEPRECATION] `provider` is deprecated. Use `email = { provider: '…' }` instead. Will be removed in 1.0.0"
|
|
49
|
+
|
|
50
|
+
@email[:provider]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def provider=(value)
|
|
54
|
+
warn "[DEPRECATION] `provider=` is deprecated. Use `email = { provider: '…' }` instead. Will be removed in 1.0.0"
|
|
55
|
+
|
|
56
|
+
@email[:provider] = value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def api_key
|
|
60
|
+
warn "[DEPRECATION] `api_key` is deprecated. Use `email = { api_key: '…' }` instead. Will be removed in 1.0.0"
|
|
61
|
+
|
|
62
|
+
@email[:api_key]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def api_key=(value)
|
|
66
|
+
warn "[DEPRECATION] `api_key=` is deprecated. Use `email = { api_key: '…' }` instead. Will be removed in 1.0.0"
|
|
67
|
+
|
|
68
|
+
@email[:api_key] = value
|
|
69
|
+
end
|
|
70
|
+
|
|
45
71
|
private
|
|
46
72
|
|
|
47
73
|
def default_email_path
|
|
@@ -31,6 +31,21 @@ module Courrier
|
|
|
31
31
|
|
|
32
32
|
def html = wrap(@html, with_layout: :html)
|
|
33
33
|
|
|
34
|
+
def to_h
|
|
35
|
+
{
|
|
36
|
+
from: @from,
|
|
37
|
+
to: @to,
|
|
38
|
+
reply_to: @reply_to,
|
|
39
|
+
cc: @cc,
|
|
40
|
+
bcc: @bcc,
|
|
41
|
+
subject: @subject,
|
|
42
|
+
text: @text,
|
|
43
|
+
html: @html,
|
|
44
|
+
auto_generate_text: @auto_generate_text,
|
|
45
|
+
layouts: @layouts
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
34
49
|
private
|
|
35
50
|
|
|
36
51
|
def wrap(content, with_layout:)
|
|
@@ -16,9 +16,24 @@ require "courrier/email/providers/userlist"
|
|
|
16
16
|
module Courrier
|
|
17
17
|
class Email
|
|
18
18
|
class Provider
|
|
19
|
+
PROVIDERS = {
|
|
20
|
+
inbox: Courrier::Email::Providers::Inbox,
|
|
21
|
+
logger: Courrier::Email::Providers::Logger,
|
|
22
|
+
loops: Courrier::Email::Providers::Loops,
|
|
23
|
+
mailgun: Courrier::Email::Providers::Mailgun,
|
|
24
|
+
mailjet: Courrier::Email::Providers::Mailjet,
|
|
25
|
+
mailpace: Courrier::Email::Providers::Mailpace,
|
|
26
|
+
postmark: Courrier::Email::Providers::Postmark,
|
|
27
|
+
resend: Courrier::Email::Providers::Resend,
|
|
28
|
+
sendgrid: Courrier::Email::Providers::Sendgrid,
|
|
29
|
+
sparkpost: Courrier::Email::Providers::Sparkpost,
|
|
30
|
+
userlist: Courrier::Email::Providers::Userlist
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
def initialize(provider: nil, api_key: nil, options: {}, provider_options: {}, context_options: {})
|
|
20
34
|
@provider = provider
|
|
21
35
|
@api_key = api_key
|
|
36
|
+
|
|
22
37
|
@options = options
|
|
23
38
|
@provider_options = provider_options
|
|
24
39
|
@context_options = context_options
|
|
@@ -26,7 +41,7 @@ module Courrier
|
|
|
26
41
|
|
|
27
42
|
def deliver
|
|
28
43
|
raise Courrier::ConfigurationError, "`provider` and `api_key` must be configured for production environment" if configuration_missing_in_production?
|
|
29
|
-
raise Courrier::ConfigurationError, "Unknown provider. Choose one of `#{comma_separated_providers}` or provide your own." if @provider.
|
|
44
|
+
raise Courrier::ConfigurationError, "Unknown provider. Choose one of `#{comma_separated_providers}` or provide your own." if @provider.nil? || @provider.to_s.strip.empty?
|
|
30
45
|
|
|
31
46
|
provider_class.new(
|
|
32
47
|
api_key: @api_key,
|
|
@@ -38,20 +53,6 @@ module Courrier
|
|
|
38
53
|
|
|
39
54
|
private
|
|
40
55
|
|
|
41
|
-
PROVIDERS = {
|
|
42
|
-
inbox: Courrier::Email::Providers::Inbox,
|
|
43
|
-
logger: Courrier::Email::Providers::Logger,
|
|
44
|
-
loops: Courrier::Email::Providers::Loops,
|
|
45
|
-
mailgun: Courrier::Email::Providers::Mailgun,
|
|
46
|
-
mailjet: Courrier::Email::Providers::Mailjet,
|
|
47
|
-
mailpace: Courrier::Email::Providers::Mailpace,
|
|
48
|
-
postmark: Courrier::Email::Providers::Postmark,
|
|
49
|
-
resend: Courrier::Email::Providers::Resend,
|
|
50
|
-
sendgrid: Courrier::Email::Providers::Sendgrid,
|
|
51
|
-
sparkpost: Courrier::Email::Providers::Sparkpost,
|
|
52
|
-
userlist: Courrier::Email::Providers::Userlist
|
|
53
|
-
}.freeze
|
|
54
|
-
|
|
55
56
|
def configuration_missing_in_production?
|
|
56
57
|
production? && required_attributes_blank?
|
|
57
58
|
end
|
|
@@ -39,7 +39,7 @@ module Courrier
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def prepare(content)
|
|
42
|
-
content.to_s.gsub(
|
|
42
|
+
content.to_s.gsub(URL_PARSER.make_regexp(%w[http https])) do |url|
|
|
43
43
|
%(<a href="#{url}">#{url}</a>)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -58,6 +58,10 @@ module Courrier
|
|
|
58
58
|
"available at #{path}"
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
URL_PARSER = (
|
|
62
|
+
defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER
|
|
63
|
+
)
|
|
64
|
+
|
|
61
65
|
class Email < Data.define(:path, :filename, :metadata)
|
|
62
66
|
Metadata = Data.define(:to, :subject)
|
|
63
67
|
|
|
@@ -21,9 +21,7 @@ module Courrier
|
|
|
21
21
|
<<~EMAIL
|
|
22
22
|
#{separator}
|
|
23
23
|
Timestamp: #{Time.now.strftime("%Y-%m-%d %H:%M:%S %z")}
|
|
24
|
-
|
|
25
|
-
To: #{@options.to}
|
|
26
|
-
Subject: #{@options.subject}
|
|
24
|
+
#{meta_fields(from: options)}
|
|
27
25
|
|
|
28
26
|
Text:
|
|
29
27
|
#{@options.text || "(empty)"}
|
|
@@ -35,6 +33,27 @@ module Courrier
|
|
|
35
33
|
end
|
|
36
34
|
|
|
37
35
|
def separator = "-" * 80
|
|
36
|
+
|
|
37
|
+
def meta_fields(from:)
|
|
38
|
+
fields = [
|
|
39
|
+
[:from, "From"],
|
|
40
|
+
[:to, "To"],
|
|
41
|
+
[:reply_to, "Reply-To"],
|
|
42
|
+
[:cc, "Cc"],
|
|
43
|
+
[:bcc, "Bcc"],
|
|
44
|
+
[:subject, "Subject"]
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
fields.map do |field, label|
|
|
48
|
+
value = from.send(field)
|
|
49
|
+
|
|
50
|
+
next if value.nil? || value.to_s.strip.empty?
|
|
51
|
+
|
|
52
|
+
"#{label}:".ljust(11) + value
|
|
53
|
+
rescue NoMatchingPatternError
|
|
54
|
+
nil
|
|
55
|
+
end.compact.join("\n")
|
|
56
|
+
end
|
|
38
57
|
end
|
|
39
58
|
end
|
|
40
59
|
end
|
|
@@ -33,10 +33,17 @@ module Courrier
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def
|
|
36
|
+
def provider_options
|
|
37
|
+
{"theme" => nil}.merge(@provider_options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def multipart_document
|
|
37
41
|
{
|
|
38
|
-
"type" => "
|
|
39
|
-
"content" =>
|
|
42
|
+
"type" => "multipart",
|
|
43
|
+
"content" => [
|
|
44
|
+
html_document,
|
|
45
|
+
text_document
|
|
46
|
+
]
|
|
40
47
|
}
|
|
41
48
|
end
|
|
42
49
|
|
|
@@ -47,19 +54,12 @@ module Courrier
|
|
|
47
54
|
}
|
|
48
55
|
end
|
|
49
56
|
|
|
50
|
-
def
|
|
57
|
+
def text_document
|
|
51
58
|
{
|
|
52
|
-
"type" => "
|
|
53
|
-
"content" =>
|
|
54
|
-
html_document,
|
|
55
|
-
text_document
|
|
56
|
-
]
|
|
59
|
+
"type" => "text",
|
|
60
|
+
"content" => @options.text
|
|
57
61
|
}
|
|
58
62
|
end
|
|
59
|
-
|
|
60
|
-
def provider_options
|
|
61
|
-
{"theme" => nil}.merge(@provider_options)
|
|
62
|
-
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
data/lib/courrier/email.rb
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "courrier/email/address"
|
|
4
|
+
require "courrier/jobs/email_delivery_job" if defined?(Rails)
|
|
4
5
|
require "courrier/email/layouts"
|
|
5
6
|
require "courrier/email/options"
|
|
6
7
|
require "courrier/email/provider"
|
|
7
8
|
|
|
8
9
|
module Courrier
|
|
9
10
|
class Email
|
|
10
|
-
attr_accessor :provider, :api_key, :default_url_options, :options
|
|
11
|
+
attr_accessor :provider, :api_key, :default_url_options, :options, :queue_options
|
|
12
|
+
|
|
13
|
+
@queue_options = {}
|
|
11
14
|
|
|
12
15
|
class << self
|
|
13
16
|
%w[provider api_key from reply_to cc bcc layouts default_url_options].each do |attribute|
|
|
14
17
|
define_method(attribute) do
|
|
15
18
|
instance_variable_get("@#{attribute}") ||
|
|
16
19
|
(superclass.respond_to?(attribute) ? superclass.send(attribute) : nil) ||
|
|
17
|
-
Courrier.configuration&.send(attribute)
|
|
20
|
+
(["provider", "api_key"].include?(attribute) ? Courrier.configuration&.email&.[](attribute.to_sym) : Courrier.configuration&.send(attribute))
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
define_method("#{attribute}=") do |value|
|
|
@@ -22,20 +25,35 @@ module Courrier
|
|
|
22
25
|
end
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
def deliver(options = {})
|
|
26
|
-
new(options).deliver_now
|
|
27
|
-
end
|
|
28
|
-
alias_method :deliver_now, :deliver
|
|
29
|
-
|
|
30
28
|
def configure(**options)
|
|
31
29
|
options.each { |key, value| send("#{key}=", value) if respond_to?("#{key}=") }
|
|
32
30
|
end
|
|
33
31
|
alias_method :set, :configure
|
|
34
32
|
|
|
35
|
-
def
|
|
33
|
+
def queue_options
|
|
34
|
+
@queue_options ||= {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_writer :queue_options
|
|
38
|
+
|
|
39
|
+
def enqueue(**options)
|
|
40
|
+
self.queue_options = options
|
|
41
|
+
end
|
|
42
|
+
alias_method :enqueue_with, :enqueue
|
|
43
|
+
|
|
44
|
+
def layout(**options)
|
|
36
45
|
self.layouts = options
|
|
37
46
|
end
|
|
38
47
|
|
|
48
|
+
def deliver(**options)
|
|
49
|
+
new(options).deliver_now
|
|
50
|
+
end
|
|
51
|
+
alias_method :deliver_now, :deliver
|
|
52
|
+
|
|
53
|
+
def deliver_later(**options)
|
|
54
|
+
new(options).deliver_later
|
|
55
|
+
end
|
|
56
|
+
|
|
39
57
|
def inherited(subclass)
|
|
40
58
|
super
|
|
41
59
|
|
|
@@ -48,8 +66,8 @@ module Courrier
|
|
|
48
66
|
end
|
|
49
67
|
|
|
50
68
|
def initialize(options = {})
|
|
51
|
-
@provider = options[:provider] || ENV["COURRIER_PROVIDER"] || self.class.provider || Courrier.configuration&.provider
|
|
52
|
-
@api_key = options[:api_key] || ENV["COURRIER_API_KEY"] || self.class.api_key || Courrier.configuration&.api_key
|
|
69
|
+
@provider = options[:provider] || ENV["COURRIER_PROVIDER"] || self.class.provider || Courrier.configuration&.email&.[](:provider)
|
|
70
|
+
@api_key = options[:api_key] || ENV["COURRIER_API_KEY"] || self.class.api_key || Courrier.configuration&.email&.[](:api_key)
|
|
53
71
|
|
|
54
72
|
@default_url_options = self.class.default_url_options.merge(options[:default_url_options] || {})
|
|
55
73
|
@context_options = options.except(:provider, :api_key, :from, :to, :reply_to, :cc, :bcc, :subject, :text, :html)
|
|
@@ -85,6 +103,30 @@ module Courrier
|
|
|
85
103
|
end
|
|
86
104
|
alias_method :deliver_now, :deliver
|
|
87
105
|
|
|
106
|
+
def deliver_later
|
|
107
|
+
if delivery_disabled?
|
|
108
|
+
Courrier.configuration&.logger&.info "[Courrier] Email delivery skipped: delivery is disabled via environment variable"
|
|
109
|
+
|
|
110
|
+
return nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
data = {
|
|
114
|
+
email_class: self.class.name,
|
|
115
|
+
provider: @provider,
|
|
116
|
+
api_key: @api_key,
|
|
117
|
+
options: @options.to_h,
|
|
118
|
+
provider_options: Courrier.configuration&.providers&.[](@provider.to_s.downcase.to_sym),
|
|
119
|
+
context_options: @context_options
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
job = Courrier::Jobs::EmailDeliveryJob
|
|
123
|
+
job = job.set(**self.class.queue_options) if self.class.queue_options.any?
|
|
124
|
+
|
|
125
|
+
job.perform_later(data)
|
|
126
|
+
rescue => error
|
|
127
|
+
raise Courrier::BackgroundDeliveryError, "Failed to enqueue email: #{error.message}"
|
|
128
|
+
end
|
|
129
|
+
|
|
88
130
|
private
|
|
89
131
|
|
|
90
132
|
def delivery_disabled?
|
data/lib/courrier/errors.rb
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module Courrier
|
|
4
4
|
class Error < StandardError; end
|
|
5
5
|
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
6
8
|
class ArgumentError < ::ArgumentError; end
|
|
7
9
|
|
|
8
10
|
class NotImplementedError < ::NotImplementedError; end
|
|
9
11
|
|
|
10
|
-
class
|
|
12
|
+
class BackgroundDeliveryError < StandardError; end
|
|
11
13
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Courrier
|
|
4
|
+
module Jobs
|
|
5
|
+
class EmailDeliveryJob < ActiveJob::Base
|
|
6
|
+
def perform(data)
|
|
7
|
+
email_class = data[:email_class].constantize
|
|
8
|
+
|
|
9
|
+
email_class.new(
|
|
10
|
+
provider: data[:provider],
|
|
11
|
+
api_key: data[:api_key],
|
|
12
|
+
from: data[:options][:from],
|
|
13
|
+
to: data[:options][:to],
|
|
14
|
+
reply_to: data[:options][:reply_to],
|
|
15
|
+
cc: data[:options][:cc],
|
|
16
|
+
bcc: data[:options][:bcc],
|
|
17
|
+
provider_options: data[:provider_options],
|
|
18
|
+
context_options: data[:context_options]
|
|
19
|
+
).deliver_now
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "courrier/subscriber/result"
|
|
4
|
+
|
|
5
|
+
module Courrier
|
|
6
|
+
class Subscriber
|
|
7
|
+
class Base
|
|
8
|
+
def initialize(api_key:)
|
|
9
|
+
@api_key = api_key
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create(email)
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def destroy(email)
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def request(method, url, body = nil)
|
|
23
|
+
uri = URI(url)
|
|
24
|
+
request_class = case method
|
|
25
|
+
when :post then Net::HTTP::Post
|
|
26
|
+
when :delete then Net::HTTP::Delete
|
|
27
|
+
when :put then Net::HTTP::Put
|
|
28
|
+
when :patch then Net::HTTP::Patch
|
|
29
|
+
when :get then Net::HTTP::Get
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
request = request_class.new(uri)
|
|
33
|
+
request.body = body.to_json if body
|
|
34
|
+
|
|
35
|
+
headers.each { |key, value| request[key] = value }
|
|
36
|
+
|
|
37
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
38
|
+
http.request(request)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Courrier::Subscriber::Result.new(response: response)
|
|
42
|
+
rescue => error
|
|
43
|
+
Courrier::Subscriber::Result.new(error: error)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def headers
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "courrier/subscriber/base"
|
|
4
|
+
|
|
5
|
+
module Courrier
|
|
6
|
+
class Subscriber
|
|
7
|
+
class Beehiiv < Base
|
|
8
|
+
ENDPOINT_URL = "https://api.beehiiv.com/v2/publications"
|
|
9
|
+
|
|
10
|
+
def create(email)
|
|
11
|
+
publication_id = Courrier.configuration.subscriber[:publication_id]
|
|
12
|
+
raise Courrier::ConfigurationError, "Beehiiv requires `publication_id` in subscriber configuration" unless publication_id
|
|
13
|
+
|
|
14
|
+
request(:post, "#{ENDPOINT_URL}/#{publication_id}/subscriptions", {"email" => email})
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def destroy(email)
|
|
18
|
+
publication_id = Courrier.configuration.subscriber[:publication_id]
|
|
19
|
+
raise Courrier::ConfigurationError, "Beehiiv requires `publication_id` in subscriber configuration" unless publication_id
|
|
20
|
+
|
|
21
|
+
subscription_id = subscription_id(publication_id, email)
|
|
22
|
+
return Courrier::Subscriber::Result.new(error: StandardError.new("Subscription not found")) unless subscription_id
|
|
23
|
+
|
|
24
|
+
request(:delete, "#{ENDPOINT_URL}/#{publication_id}/subscriptions/#{subscription_id}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def subscription_id(publication_id, email)
|
|
30
|
+
response = request(:get, "#{ENDPOINT_URL}/#{publication_id}/subscriptions?email=#{email}")
|
|
31
|
+
|
|
32
|
+
return nil unless response.success?
|
|
33
|
+
|
|
34
|
+
response.data.dig("data", 0, "id")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def headers
|
|
38
|
+
{
|
|
39
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
40
|
+
"Content-Type" => "application/json"
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "courrier/subscriber/base"
|
|
4
|
+
|
|
5
|
+
module Courrier
|
|
6
|
+
class Subscriber
|
|
7
|
+
class Buttondown < Base
|
|
8
|
+
ENDPOINT_URL = "https://api.buttondown.email/v1/subscribers"
|
|
9
|
+
|
|
10
|
+
def create(email)
|
|
11
|
+
request(:post, ENDPOINT_URL, {"email" => email})
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def destroy(email)
|
|
15
|
+
request(:delete, "#{ENDPOINT_URL}/#{email}")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def headers
|
|
21
|
+
{
|
|
22
|
+
"Authorization" => "Token #{@api_key}",
|
|
23
|
+
"Content-Type" => "application/json"
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "courrier/subscriber/base"
|
|
4
|
+
|
|
5
|
+
module Courrier
|
|
6
|
+
class Subscriber
|
|
7
|
+
class Kit < Base
|
|
8
|
+
ENDPOINT_URL = "https://api.convertkit.com/v3/forms"
|
|
9
|
+
|
|
10
|
+
def create(email)
|
|
11
|
+
form_id = Courrier.configuration.subscriber[:form_id]
|
|
12
|
+
raise Courrier::ConfigurationError, "Kit requires `form_id` in subscriber configuration" unless form_id
|
|
13
|
+
|
|
14
|
+
request(:post, "#{ENDPOINT_URL}/#{form_id}/subscribe", {
|
|
15
|
+
"api_key" => @api_key,
|
|
16
|
+
"email" => email
|
|
17
|
+
})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy(email)
|
|
21
|
+
request(:put, "https://api.convertkit.com/v3/unsubscribe", {
|
|
22
|
+
"api_secret" => @api_key,
|
|
23
|
+
"email" => email
|
|
24
|
+
})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def headers
|
|
30
|
+
{
|
|
31
|
+
"Content-Type" => "application/json"
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "courrier/subscriber/base"
|
|
4
|
+
|
|
5
|
+
module Courrier
|
|
6
|
+
class Subscriber
|
|
7
|
+
class Loops < Base
|
|
8
|
+
ENDPOINT_URL = "https://app.loops.so/api/v1/contacts"
|
|
9
|
+
|
|
10
|
+
def create(email)
|
|
11
|
+
request(:post, "#{ENDPOINT_URL}/create", {
|
|
12
|
+
"email" => email
|
|
13
|
+
})
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def destroy(email)
|
|
17
|
+
request(:post, "#{ENDPOINT_URL}/delete", {
|
|
18
|
+
"email" => email
|
|
19
|
+
})
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def headers
|
|
25
|
+
{
|
|
26
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
27
|
+
"Content-Type" => "application/json"
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "courrier/subscriber/base"
|
|
4
|
+
|
|
5
|
+
module Courrier
|
|
6
|
+
class Subscriber
|
|
7
|
+
class Mailchimp < Base
|
|
8
|
+
def create(email)
|
|
9
|
+
dc = Courrier.configuration.subscriber[:dc]
|
|
10
|
+
list_id = Courrier.configuration.subscriber[:list_id]
|
|
11
|
+
|
|
12
|
+
raise Courrier::ConfigurationError, "Mailchimp requires `dc` and `list_id` in subscriber configuration" unless dc && list_id
|
|
13
|
+
|
|
14
|
+
request(:post, "https://#{dc}.api.mailchimp.com/3.0/lists/#{list_id}/members", {
|
|
15
|
+
"email_address" => email,
|
|
16
|
+
"status" => "subscribed"
|
|
17
|
+
})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def destroy(email)
|
|
21
|
+
dc = Courrier.configuration.subscriber[:dc]
|
|
22
|
+
list_id = Courrier.configuration.subscriber[:list_id]
|
|
23
|
+
|
|
24
|
+
raise Courrier::ConfigurationError, "Mailchimp requires `dc` and `list_id` in subscriber configuration" unless dc && list_id
|
|
25
|
+
|
|
26
|
+
request(:delete, "https://#{dc}.api.mailchimp.com/3.0/lists/#{list_id}/members/#{email}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def headers
|
|
32
|
+
{
|
|
33
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
34
|
+
"Content-Type" => "application/json"
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "courrier/subscriber/base"
|
|
4
|
+
|
|
5
|
+
module Courrier
|
|
6
|
+
class Subscriber
|
|
7
|
+
class Mailerlite < Base
|
|
8
|
+
ENDPOINT_URL = "https://connect.mailerlite.com/api/subscribers"
|
|
9
|
+
|
|
10
|
+
def create(email)
|
|
11
|
+
request(:post, ENDPOINT_URL, {"email" => email})
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def destroy(email)
|
|
15
|
+
request(:delete, "#{ENDPOINT_URL}/#{email}")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def headers
|
|
21
|
+
{
|
|
22
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
23
|
+
"Content-Type" => "application/json"
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Courrier
|
|
4
|
+
class Subscriber
|
|
5
|
+
class Result
|
|
6
|
+
attr_reader :success, :response, :data, :error
|
|
7
|
+
|
|
8
|
+
def initialize(response: nil, error: nil)
|
|
9
|
+
@response = response
|
|
10
|
+
@error = error
|
|
11
|
+
@data = parsed(@response&.body)
|
|
12
|
+
@success = successful?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def success? = @success
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def parsed(body)
|
|
20
|
+
return {} if @response.nil?
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
JSON.parse(body)
|
|
24
|
+
rescue JSON::ParserError
|
|
25
|
+
{}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def successful?
|
|
30
|
+
return false if response_failed?
|
|
31
|
+
return @data["success"] if @data.key?("success")
|
|
32
|
+
|
|
33
|
+
(200..299).cover?(status_code)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def response_failed? = @error || @response.nil?
|
|
37
|
+
|
|
38
|
+
def status_code = @response.code.to_i
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Courrier
|
|
4
|
+
class Subscriber
|
|
5
|
+
class << self
|
|
6
|
+
def create(email)
|
|
7
|
+
provider.create(email)
|
|
8
|
+
end
|
|
9
|
+
alias_method :add, :create
|
|
10
|
+
|
|
11
|
+
def destroy(email)
|
|
12
|
+
provider.destroy(email)
|
|
13
|
+
end
|
|
14
|
+
alias_method :delete, :destroy
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def provider
|
|
19
|
+
@provider ||= provider_class.new(
|
|
20
|
+
api_key: Courrier.configuration.subscriber[:api_key]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def provider_class
|
|
25
|
+
provider_name = Courrier.configuration.subscriber[:provider]
|
|
26
|
+
|
|
27
|
+
return provider_name if provider_name.is_a?(Class)
|
|
28
|
+
require "courrier/subscriber/#{provider_name}"
|
|
29
|
+
|
|
30
|
+
Object.const_get("Courrier::Subscriber::#{provider_name.capitalize}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/courrier/version.rb
CHANGED
data/lib/courrier.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module Courrier
|
|
2
2
|
class EmailGenerator < Rails::Generators::NamedBase
|
|
3
|
+
AVAILABLE_TEMPLATES = %w[welcome password_reset]
|
|
4
|
+
|
|
3
5
|
desc "Create a new Courrier Email class"
|
|
4
6
|
|
|
5
7
|
source_root File.expand_path("templates", __dir__)
|
|
@@ -7,13 +9,34 @@ module Courrier
|
|
|
7
9
|
check_class_collision suffix: "Email"
|
|
8
10
|
|
|
9
11
|
class_option :skip_suffix, type: :boolean, default: false
|
|
12
|
+
class_option :template, type: :string, desc: "Template type (#{AVAILABLE_TEMPLATES.join(", ")})"
|
|
10
13
|
|
|
11
14
|
def copy_mailer_file
|
|
12
|
-
template
|
|
15
|
+
template template_file, destination_path
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
private
|
|
16
19
|
|
|
20
|
+
def file_name = super.delete_suffix("_email")
|
|
21
|
+
|
|
17
22
|
def parent_class = defined?(ApplicationEmail) ? ApplicationEmail : Courrier::Email
|
|
23
|
+
|
|
24
|
+
def template_file
|
|
25
|
+
if options[:template] && template_exists?("email/#{options[:template]}.rb.tt")
|
|
26
|
+
"email/#{options[:template]}.rb.tt"
|
|
27
|
+
else
|
|
28
|
+
"email.rb.tt"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def destination_path
|
|
33
|
+
File.join(Courrier.configuration.email_path, class_path, "#{file_name}#{options[:skip_suffix] ? "" : "_email"}.rb")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def template_exists?(path)
|
|
37
|
+
find_in_source_paths(path)
|
|
38
|
+
rescue
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
18
41
|
end
|
|
19
42
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Usage:
|
|
2
|
+
#
|
|
3
|
+
# PasswordResetEmail.deliver to: user.email, reset_url: edit_password_url(user.password_reset_token)
|
|
4
|
+
#
|
|
5
|
+
class <%= class_name %><%= options[:skip_suffix] ? "" : "Email" %> < <%= parent_class %>
|
|
6
|
+
def subject = "Reset your password"
|
|
7
|
+
|
|
8
|
+
def text
|
|
9
|
+
<<~TEXT
|
|
10
|
+
You can reset your password within the next 15 minutes on this password reset page:
|
|
11
|
+
#{reset_url}
|
|
12
|
+
|
|
13
|
+
If you didn't request a password reset, please ignore this email.
|
|
14
|
+
TEXT
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def html
|
|
18
|
+
<<~HTML
|
|
19
|
+
<p>
|
|
20
|
+
You can reset your password within the next 15 minutes on
|
|
21
|
+
<a href="#{reset_url}">this password reset page</a>.
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p>
|
|
25
|
+
If you didn't request a password reset, please ignore this email.
|
|
26
|
+
</p>
|
|
27
|
+
HTML
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Usage:
|
|
2
|
+
#
|
|
3
|
+
# WelcomeEmail.deliver to: user.email, name: "John", login_url: "https://example.com/login"
|
|
4
|
+
#
|
|
5
|
+
class <%= class_name %><%= options[:skip_suffix] ? "" : "Email" %> < <%= parent_class %>
|
|
6
|
+
def subject = "Welcome to My First App, #{name}!"
|
|
7
|
+
|
|
8
|
+
def text
|
|
9
|
+
<<~TEXT
|
|
10
|
+
Welcome, #{name}!
|
|
11
|
+
|
|
12
|
+
We're excited to have you on board. Here's how to get started:
|
|
13
|
+
|
|
14
|
+
1. Log in to your account: #{login_url}
|
|
15
|
+
2. Complete your profile
|
|
16
|
+
3. Explore our features
|
|
17
|
+
|
|
18
|
+
If you have any questions, our help center is available to assist you.
|
|
19
|
+
|
|
20
|
+
Thanks for joining us!
|
|
21
|
+
TEXT
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def html
|
|
25
|
+
<<~HTML
|
|
26
|
+
<p>Welcome, #{name}!</p>
|
|
27
|
+
|
|
28
|
+
<p>We're excited to have you on board. Here's how to get started:</p>
|
|
29
|
+
|
|
30
|
+
<ol>
|
|
31
|
+
<li><a href="#{login_url}">Log in to your account</a></li>
|
|
32
|
+
<li>Complete your profile</li>
|
|
33
|
+
<li>Explore our features</li>
|
|
34
|
+
</ol>
|
|
35
|
+
|
|
36
|
+
<p>If you have any questions, our help center is available to assist you.</p>
|
|
37
|
+
|
|
38
|
+
<p>
|
|
39
|
+
Thanks for joining us!
|
|
40
|
+
</p>
|
|
41
|
+
HTML
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
Courrier.configure do |config|
|
|
2
2
|
include Courrier::Email::Address
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
4
|
+
# Set your email delivery provider
|
|
5
|
+
# config.email = {
|
|
6
|
+
# provider = "", # default, `logger`, choose from: <%= Courrier::Email::Provider::PROVIDERS.keys.join(", ") %>
|
|
7
|
+
# api_key = "" your transactional email provider's API key
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
# Set your marketing email provider
|
|
11
|
+
# config.subscriber = {
|
|
12
|
+
# provider = ""
|
|
13
|
+
}
|
|
7
14
|
|
|
8
|
-
# Add your email provider's API key
|
|
9
|
-
# config.api_key = ""
|
|
10
15
|
|
|
11
16
|
# Configure provider-specific settings
|
|
12
17
|
# config.providers.loops.transactional_id = ""
|
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.8.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: 2025-
|
|
11
|
+
date: 2025-12-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: launchy
|
|
@@ -97,12 +97,24 @@ files:
|
|
|
97
97
|
- lib/courrier/email/transformer.rb
|
|
98
98
|
- lib/courrier/engine.rb
|
|
99
99
|
- lib/courrier/errors.rb
|
|
100
|
+
- lib/courrier/jobs/email_delivery_job.rb
|
|
100
101
|
- lib/courrier/railtie.rb
|
|
102
|
+
- lib/courrier/subscriber.rb
|
|
103
|
+
- lib/courrier/subscriber/base.rb
|
|
104
|
+
- lib/courrier/subscriber/beehiiv.rb
|
|
105
|
+
- lib/courrier/subscriber/buttondown.rb
|
|
106
|
+
- lib/courrier/subscriber/kit.rb
|
|
107
|
+
- lib/courrier/subscriber/loops.rb
|
|
108
|
+
- lib/courrier/subscriber/mailchimp.rb
|
|
109
|
+
- lib/courrier/subscriber/mailerlite.rb
|
|
110
|
+
- lib/courrier/subscriber/result.rb
|
|
101
111
|
- lib/courrier/tasks/courrier.rake
|
|
102
112
|
- lib/courrier/version.rb
|
|
103
113
|
- lib/generators/courrier/email_generator.rb
|
|
104
114
|
- lib/generators/courrier/install_generator.rb
|
|
105
115
|
- lib/generators/courrier/templates/email.rb.tt
|
|
116
|
+
- lib/generators/courrier/templates/email/password_reset.rb.tt
|
|
117
|
+
- lib/generators/courrier/templates/email/welcome.rb.tt
|
|
106
118
|
- lib/generators/courrier/templates/initializer.rb.tt
|
|
107
119
|
homepage: https://railsdesigner.com/courrier/
|
|
108
120
|
licenses:
|