courrier 0.6.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +159 -26
  4. data/app/controllers/courrier/previews/cleanups_controller.rb +13 -0
  5. data/app/controllers/courrier/previews_controller.rb +23 -0
  6. data/app/views/courrier/previews/index.html.erb +171 -0
  7. data/config/routes.rb +8 -0
  8. data/courrier.gemspec +3 -3
  9. data/lib/courrier/configuration/{preview.rb → inbox.rb} +3 -3
  10. data/lib/courrier/configuration.rb +32 -6
  11. data/lib/courrier/email/options.rb +15 -0
  12. data/lib/courrier/email/provider.rb +22 -17
  13. data/lib/courrier/email/providers/base.rb +2 -1
  14. data/lib/courrier/email/providers/{preview → inbox}/default.html.erb +13 -6
  15. data/lib/courrier/email/providers/inbox.rb +83 -0
  16. data/lib/courrier/email/providers/logger.rb +22 -3
  17. data/lib/courrier/email/providers/loops.rb +1 -8
  18. data/lib/courrier/email/providers/resend.rb +32 -0
  19. data/lib/courrier/email/providers/userlist.rb +13 -13
  20. data/lib/courrier/email.rb +54 -11
  21. data/lib/courrier/engine.rb +7 -0
  22. data/lib/courrier/errors.rb +3 -1
  23. data/lib/courrier/jobs/email_delivery_job.rb +23 -0
  24. data/lib/courrier/subscriber/base.rb +51 -0
  25. data/lib/courrier/subscriber/beehiiv.rb +45 -0
  26. data/lib/courrier/subscriber/buttondown.rb +28 -0
  27. data/lib/courrier/subscriber/kit.rb +36 -0
  28. data/lib/courrier/subscriber/loops.rb +32 -0
  29. data/lib/courrier/subscriber/mailchimp.rb +39 -0
  30. data/lib/courrier/subscriber/mailerlite.rb +28 -0
  31. data/lib/courrier/subscriber/result.rb +41 -0
  32. data/lib/courrier/subscriber.rb +34 -0
  33. data/lib/courrier/tasks/courrier.rake +2 -2
  34. data/lib/courrier/version.rb +1 -1
  35. data/lib/courrier.rb +2 -0
  36. data/lib/generators/courrier/email_generator.rb +24 -1
  37. data/lib/generators/courrier/templates/email/password_reset.rb.tt +29 -0
  38. data/lib/generators/courrier/templates/email/welcome.rb.tt +43 -0
  39. data/lib/generators/courrier/templates/initializer.rb.tt +10 -5
  40. metadata +26 -8
  41. data/lib/courrier/email/providers/preview.rb +0 -51
@@ -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
@@ -1,13 +1,13 @@
1
1
  namespace :tmp do
2
2
  task :courrier do
3
- rm_rf Dir["#{Courrier.configuration.preview.destination}/[^.]*"], verbose: false
3
+ rm_rf Dir["#{Courrier.configuration.inbox.destination}/[^.]*"], verbose: false
4
4
  end
5
5
 
6
6
  task clear: :courrier
7
7
  end
8
8
 
9
9
  namespace :courrier do
10
- desc "Clear preview email files from `#{Courrier.configuration.preview.destination}`"
10
+ desc "Clear email files from `#{Courrier.configuration.inbox.destination}`"
11
11
 
12
12
  task clear: "tmp:courrier"
13
13
  end
@@ -1,3 +1,3 @@
1
1
  module Courrier
2
- VERSION = "0.6.0"
2
+ VERSION = "0.8.0"
3
3
  end
data/lib/courrier.rb CHANGED
@@ -4,6 +4,8 @@ require "courrier/version"
4
4
  require "courrier/errors"
5
5
  require "courrier/configuration"
6
6
  require "courrier/email"
7
+ require "courrier/subscriber"
8
+ require "courrier/engine" if defined?(Rails)
7
9
  require "courrier/railtie" if defined?(Rails)
8
10
 
9
11
  module Courrier
@@ -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 "email.rb", File.join(Courrier.configuration.email_path, class_path, "#{file_name}#{options[:skip_suffix] ? "" : "_email"}.rb")
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
- # Choose your email delivery provider
5
- # Default: `logger`
6
- # config.provider = ""
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.6.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-05-12 00:00:00.000000000 Z
11
+ date: 2025-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: launchy
@@ -50,8 +50,8 @@ dependencies:
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
52
  version: '2'
53
- description: Modern, API-powered email delivery for Ruby apps with support for Postmark,
54
- SendGrid, Mailgun and more.
53
+ description: API-powered email delivery for Ruby apps with support for Postmark, SendGrid,
54
+ Mailgun and more.
55
55
  email:
56
56
  - devs@railsdesigner.com
57
57
  executables: []
@@ -62,13 +62,17 @@ files:
62
62
  - Gemfile.lock
63
63
  - README.md
64
64
  - Rakefile
65
+ - app/controllers/courrier/previews/cleanups_controller.rb
66
+ - app/controllers/courrier/previews_controller.rb
67
+ - app/views/courrier/previews/index.html.erb
65
68
  - bin/console
66
69
  - bin/release
67
70
  - bin/setup
71
+ - config/routes.rb
68
72
  - courrier.gemspec
69
73
  - lib/courrier.rb
70
74
  - lib/courrier/configuration.rb
71
- - lib/courrier/configuration/preview.rb
75
+ - lib/courrier/configuration/inbox.rb
72
76
  - lib/courrier/configuration/providers.rb
73
77
  - lib/courrier/email.rb
74
78
  - lib/courrier/email/address.rb
@@ -76,27 +80,41 @@ files:
76
80
  - lib/courrier/email/options.rb
77
81
  - lib/courrier/email/provider.rb
78
82
  - lib/courrier/email/providers/base.rb
83
+ - lib/courrier/email/providers/inbox.rb
84
+ - lib/courrier/email/providers/inbox/default.html.erb
79
85
  - lib/courrier/email/providers/logger.rb
80
86
  - lib/courrier/email/providers/loops.rb
81
87
  - lib/courrier/email/providers/mailgun.rb
82
88
  - lib/courrier/email/providers/mailjet.rb
83
89
  - lib/courrier/email/providers/mailpace.rb
84
90
  - lib/courrier/email/providers/postmark.rb
85
- - lib/courrier/email/providers/preview.rb
86
- - lib/courrier/email/providers/preview/default.html.erb
91
+ - lib/courrier/email/providers/resend.rb
87
92
  - lib/courrier/email/providers/sendgrid.rb
88
93
  - lib/courrier/email/providers/sparkpost.rb
89
94
  - lib/courrier/email/providers/userlist.rb
90
95
  - lib/courrier/email/request.rb
91
96
  - lib/courrier/email/result.rb
92
97
  - lib/courrier/email/transformer.rb
98
+ - lib/courrier/engine.rb
93
99
  - lib/courrier/errors.rb
100
+ - lib/courrier/jobs/email_delivery_job.rb
94
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
95
111
  - lib/courrier/tasks/courrier.rake
96
112
  - lib/courrier/version.rb
97
113
  - lib/generators/courrier/email_generator.rb
98
114
  - lib/generators/courrier/install_generator.rb
99
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
100
118
  - lib/generators/courrier/templates/initializer.rb.tt
101
119
  homepage: https://railsdesigner.com/courrier/
102
120
  licenses:
@@ -122,5 +140,5 @@ requirements: []
122
140
  rubygems_version: 3.4.1
123
141
  signing_key:
124
142
  specification_version: 4
125
- summary: Modern, API-powered email delivery for Rails apps
143
+ summary: API-powered email delivery for Ruby apps
126
144
  test_files: []
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tmpdir"
4
- require "fileutils"
5
- require "launchy"
6
-
7
- module Courrier
8
- class Email
9
- module Providers
10
- class Preview < Base
11
- def deliver
12
- FileUtils.mkdir_p(config.destination)
13
-
14
- file_path = File.join(config.destination, "#{Time.now.to_i}.html")
15
-
16
- File.write(file_path, ERB.new(File.read(config.template_path)).result(binding))
17
-
18
- Launchy.open(file_path)
19
-
20
- "Preview email saved to #{file_path}#{config.auto_open ? " and opened in browser" : ""}"
21
- end
22
-
23
- def name = extract(@options.to)[:name]
24
-
25
- def email = extract(@options.to)[:email]
26
-
27
- def text = prepare(@options.text)
28
-
29
- def html = prepare(@options.html)
30
-
31
- private
32
-
33
- def extract(to)
34
- if to.to_s =~ /(.*?)\s*<(.+?)>/
35
- {name: $1.strip, email: $2.strip}
36
- else
37
- {name: nil, email: to.to_s.strip}
38
- end
39
- end
40
-
41
- def prepare(content)
42
- content.to_s.gsub(URI::DEFAULT_PARSER.make_regexp(%w[http https])) do |url|
43
- %(<a href="#{url}">#{url}</a>)
44
- end
45
- end
46
-
47
- def config = @config ||= Courrier.configuration.preview
48
- end
49
- end
50
- end
51
- end