courrier 0.5.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 +7 -0
  2. data/Gemfile +15 -0
  3. data/Gemfile.lock +109 -0
  4. data/README.md +243 -0
  5. data/Rakefile +9 -0
  6. data/bin/console +11 -0
  7. data/bin/release +19 -0
  8. data/bin/setup +8 -0
  9. data/courrier.gemspec +25 -0
  10. data/lib/courrier/configuration/preview.rb +23 -0
  11. data/lib/courrier/configuration/providers.rb +41 -0
  12. data/lib/courrier/configuration.rb +53 -0
  13. data/lib/courrier/email/address.rb +37 -0
  14. data/lib/courrier/email/layouts.rb +40 -0
  15. data/lib/courrier/email/options.rb +55 -0
  16. data/lib/courrier/email/provider.rb +68 -0
  17. data/lib/courrier/email/providers/base.rb +39 -0
  18. data/lib/courrier/email/providers/logger.rb +41 -0
  19. data/lib/courrier/email/providers/loops.rb +36 -0
  20. data/lib/courrier/email/providers/mailgun.rb +36 -0
  21. data/lib/courrier/email/providers/mailjet.rb +50 -0
  22. data/lib/courrier/email/providers/mailpace.rb +32 -0
  23. data/lib/courrier/email/providers/postmark.rb +34 -0
  24. data/lib/courrier/email/providers/preview/default.html.erb +119 -0
  25. data/lib/courrier/email/providers/preview.rb +51 -0
  26. data/lib/courrier/email/providers/sendgrid.rb +57 -0
  27. data/lib/courrier/email/providers/sparkpost.rb +38 -0
  28. data/lib/courrier/email/request.rb +73 -0
  29. data/lib/courrier/email/result.rb +43 -0
  30. data/lib/courrier/email/transformer.rb +46 -0
  31. data/lib/courrier/email.rb +97 -0
  32. data/lib/courrier/errors.rb +11 -0
  33. data/lib/courrier/railtie.rb +23 -0
  34. data/lib/courrier/tasks/courrier.rake +13 -0
  35. data/lib/courrier/version.rb +3 -0
  36. data/lib/courrier.rb +10 -0
  37. data/lib/generators/courrier/email_generator.rb +19 -0
  38. data/lib/generators/courrier/install_generator.rb +11 -0
  39. data/lib/generators/courrier/templates/email.rb.tt +15 -0
  40. data/lib/generators/courrier/templates/initializer.rb.tt +38 -0
  41. metadata +125 -0
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "courrier/email/result"
6
+
7
+ module Courrier
8
+ class Email
9
+ class Request
10
+ def initialize(endpoint_url:, body:, provider:, headers: {}, content_type: "application/json")
11
+ @endpoint_url = endpoint_url
12
+ @body = body
13
+ @provider = provider
14
+ @headers = headers
15
+ @content_type = content_type
16
+ end
17
+
18
+ def post
19
+ uri = URI.parse(@endpoint_url)
20
+ request = Net::HTTP::Post.new(uri)
21
+
22
+ headers_for request
23
+ body_for request
24
+
25
+ options = {use_ssl: uri.scheme == "https"}
26
+
27
+ begin
28
+ response = Net::HTTP.start(uri.hostname, uri.port, options) { _1.request(request) }
29
+
30
+ Result.new(response: response)
31
+ rescue => error
32
+ Result.new(error: error)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def headers_for(request)
39
+ default_headers.merge(@headers).each do |key, value|
40
+ request[key] = value
41
+ end
42
+ end
43
+
44
+ def body_for(request)
45
+ if requires_multipart_form?
46
+ set_multipart_form(request)
47
+ else
48
+ set_json_body(request)
49
+ end
50
+ end
51
+
52
+ def default_headers
53
+ {
54
+ "Content-Type" => @content_type,
55
+ "Accept" => "application/json"
56
+ }
57
+ end
58
+
59
+ def requires_multipart_form?
60
+ ["Mailgun"].include?(@provider)
61
+ end
62
+
63
+ def set_multipart_form(request)
64
+ request.set_form(@body, "multipart/form-data")
65
+ end
66
+
67
+ def set_json_body(request)
68
+ request.content_type = "application/json"
69
+ request.body = JSON.dump(@body)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Courrier
6
+ class Email
7
+ class Result
8
+ attr_reader :success, :response, :data, :error
9
+
10
+ def initialize(response: nil, error: nil)
11
+ @response = response
12
+ @error = error
13
+ @data = parse_body(@response&.body)
14
+ @success = successful?
15
+ end
16
+
17
+ def success? = @success
18
+
19
+ private
20
+
21
+ def parse_body(body)
22
+ return {} if @response.nil?
23
+
24
+ begin
25
+ JSON.parse(body)
26
+ rescue JSON::ParserError
27
+ {}
28
+ end
29
+ end
30
+
31
+ def successful?
32
+ return false if response_failed?
33
+ return @data["success"] if @data.key?("success")
34
+
35
+ (200..299).cover?(status_code)
36
+ end
37
+
38
+ def response_failed? = @error || @response.nil?
39
+
40
+ def status_code = @response.code.to_i
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module Courrier
6
+ class Email
7
+ class Transformer
8
+ def initialize(content)
9
+ @content = content
10
+ end
11
+
12
+ def to_text
13
+ Nokogiri::HTML(@content)
14
+ .then { remove_unwanted_elements(_1) }
15
+ .then { process_links(_1) }
16
+ .then { preserve_line_breaks(_1) }
17
+ .then { clean_up(_1) }
18
+ end
19
+
20
+ private
21
+
22
+ BLOCK_ELEMENTS = %w[p div h1 h2 h3 h4 h5 h6 pre blockquote li]
23
+
24
+ def remove_unwanted_elements(html) = html.tap { _1.css("script, style").remove }
25
+
26
+ def process_links(html)
27
+ html.tap do |document|
28
+ document.css("a")
29
+ .select { valid?(_1) }
30
+ .reject { _1.text.strip.empty? || _1.text.strip == _1["href"] }
31
+ .each { _1.content = "#{_1.text.strip} (#{_1["href"]})" }
32
+ end
33
+ end
34
+
35
+ def preserve_line_breaks(html)
36
+ html.tap do |document|
37
+ document.css(BLOCK_ELEMENTS.join(",")).each { _1.after("\n") }
38
+ end
39
+ end
40
+
41
+ def clean_up(html) = html.text.strip.gsub(/ *\n */m, "\n").squeeze(" \n")
42
+
43
+ def valid?(link) = link["href"] && !link["href"].start_with?("#")
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "courrier/email/address"
4
+ require "courrier/email/layouts"
5
+ require "courrier/email/options"
6
+ require "courrier/email/provider"
7
+
8
+ module Courrier
9
+ class Email
10
+ attr_accessor :provider, :api_key, :default_url_options, :options
11
+
12
+ class << self
13
+ %w[provider api_key from reply_to cc bcc layouts default_url_options].each do |attribute|
14
+ define_method(attribute) do
15
+ instance_variable_get("@#{attribute}") ||
16
+ (superclass.respond_to?(attribute) ? superclass.send(attribute) : nil) ||
17
+ Courrier.configuration&.send(attribute)
18
+ end
19
+
20
+ define_method("#{attribute}=") do |value|
21
+ instance_variable_set("@#{attribute}", value)
22
+ end
23
+ end
24
+
25
+ def deliver(options = {})
26
+ new(options).deliver_now
27
+ end
28
+ alias_method :deliver_now, :deliver
29
+
30
+ def configure(**options)
31
+ options.each { |key, value| send("#{key}=", value) if respond_to?("#{key}=") }
32
+ end
33
+ alias_method :set, :configure
34
+
35
+ def layout(options = {})
36
+ self.layouts = options
37
+ end
38
+
39
+ def inherited(subclass)
40
+ super
41
+
42
+ # If you read this and know how to move this Rails-specific logic somewhere
43
+ # else, e.g. `lib/courrier/railtie.rb`, open a PR ❤️
44
+ if defined?(Rails) && Rails.application
45
+ subclass.include Rails.application.routes.url_helpers
46
+ end
47
+ end
48
+ end
49
+
50
+ def initialize(options = {})
51
+ @provider = options[:provider] || ENV["COURRIER_PROVIDER"] || self.class.provider || Courrier.configuration&.provider
52
+ @api_key = options[:api_key] || ENV["COURRIER_PROVIDER"] || self.class.api_key || Courrier.configuration&.api_key
53
+
54
+ @default_url_options = self.class.default_url_options.merge(options[:default_url_options] || {})
55
+ @context_options = options.except(:provider, :api_key, :from, :to, :reply_to, :cc, :bcc, :subject, :text, :html)
56
+ @options = Email::Options.new(
57
+ options.merge(
58
+ from: options[:from] || self.class.from || Courrier.configuration&.from,
59
+ reply_to: options[:reply_to] || self.class.reply_to || Courrier.configuration&.reply_to,
60
+ cc: options[:cc] || self.class.cc || Courrier.configuration&.cc,
61
+ bcc: options[:bcc] || self.class.bcc || Courrier.configuration&.bcc,
62
+ subject: subject,
63
+ text: text,
64
+ html: html,
65
+ auto_generate_text: Courrier.configuration&.auto_generate_text,
66
+ layouts: Courrier::Email::Layouts.new(self).build
67
+ )
68
+ )
69
+ end
70
+
71
+ def deliver
72
+ if delivery_disabled?
73
+ Courrier.configuration&.logger&.info "[Courrier] Email delivery skipped: delivery is disabled via environment variable"
74
+
75
+ return nil
76
+ end
77
+
78
+ Provider.new(
79
+ provider: @provider,
80
+ api_key: @api_key,
81
+ options: @options,
82
+ provider_options: Courrier.configuration&.providers&.[](@provider.to_s.downcase.to_sym)&.to_h
83
+ ).deliver
84
+ end
85
+ alias_method :deliver_now, :deliver
86
+
87
+ private
88
+
89
+ def delivery_disabled?
90
+ ENV["COURRIER_EMAIL_DISABLED"] == "true" || ENV["COURRIER_EMAIL_ENABLED"] == "false"
91
+ end
92
+
93
+ def method_missing(name, *) = @context_options[name]
94
+
95
+ def respond_to_missing?(name, include_private = false) = true
96
+ end
97
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Courrier
4
+ class Error < StandardError; end
5
+
6
+ class ArgumentError < ::ArgumentError; end
7
+
8
+ class NotImplementedError < ::NotImplementedError; end
9
+
10
+ class ConfigurationError < Error; end
11
+ end
@@ -0,0 +1,23 @@
1
+ module Courrier
2
+ class Railtie < Rails::Railtie
3
+ config.after_initialize do
4
+ Courrier::Email.default_url_options = Courrier.configuration.default_url_options
5
+ end
6
+
7
+ ActiveSupport.on_load(:action_view) do
8
+ include Courrier::Email::Address
9
+ end
10
+
11
+ ActiveSupport.on_load(:action_controller) do
12
+ include Courrier::Email::Address
13
+ end
14
+
15
+ ActiveSupport.on_load(:active_job) do
16
+ include Courrier::Email::Address
17
+ end
18
+
19
+ rake_tasks do
20
+ load File.expand_path("../tasks/courrier.rake", __FILE__)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ namespace :tmp do
2
+ task :courrier do
3
+ rm_rf Dir["#{Courrier.configuration.preview.destination}/[^.]*"], verbose: false
4
+ end
5
+
6
+ task clear: :courrier
7
+ end
8
+
9
+ namespace :courrier do
10
+ desc "Clear preview email files from `#{Courrier.configuration.preview.destination}`"
11
+
12
+ task clear: "tmp:courrier"
13
+ end
@@ -0,0 +1,3 @@
1
+ module Courrier
2
+ VERSION = "0.5.0"
3
+ end
data/lib/courrier.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "courrier/version"
4
+ require "courrier/errors"
5
+ require "courrier/configuration"
6
+ require "courrier/email"
7
+ require "courrier/railtie" if defined?(Rails)
8
+
9
+ module Courrier
10
+ end
@@ -0,0 +1,19 @@
1
+ module Courrier
2
+ class EmailGenerator < Rails::Generators::NamedBase
3
+ desc "Create a new Courrier Email class"
4
+
5
+ source_root File.expand_path("templates", __dir__)
6
+
7
+ check_class_collision suffix: "Email"
8
+
9
+ class_option :skip_suffix, type: :boolean, default: false
10
+
11
+ def copy_mailer_file
12
+ template "email.rb", File.join(Courrier.configuration.email_path, class_path, "#{file_name}#{options[:skip_suffix] ? "" : "_email"}.rb")
13
+ end
14
+
15
+ private
16
+
17
+ def parent_class = defined?(ApplicationEmail) ? ApplicationEmail : Courrier::Email
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module Courrier
2
+ class InstallGenerator < Rails::Generators::Base
3
+ desc "Creates the initializer for Courrier"
4
+
5
+ source_root File.expand_path("templates", __dir__)
6
+
7
+ def copy_initializer_file
8
+ template "initializer.rb", "config/initializers/courrier.rb"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ class <%= class_name %><%= options[:skip_suffix] ? "" : "Email" %> < <%= parent_class %>
2
+ def subject = ""
3
+
4
+ # Create HTML and text emails using:
5
+ # https://railsdesigner.com/minimal-email-editor/
6
+ def text
7
+ <<~TEXT
8
+ TEXT
9
+ end
10
+
11
+ def html
12
+ <<~HTML
13
+ HTML
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ Courrier.configure do |config|
2
+ include Courrier::Email::Address
3
+
4
+ # Choose your email delivery provider
5
+ # Default: `logger`
6
+ # config.provider = ""
7
+
8
+ # Add your email provider's API key
9
+ # config.api_key = ""
10
+
11
+ # Configure provider-specific settings
12
+ # config.providers.loops.transactional_id = ""
13
+ # config.providers.mailgun.domain = ""
14
+
15
+
16
+ # Set default sender details
17
+ config.from = email_with_name("support@example.com", "Example Support") # => `Example Support <support@example.com>`
18
+ # config.reply_to = ""
19
+ # config.cc = ""
20
+ # config.bcc = ""
21
+
22
+
23
+ # Set host for Rails URL helpers, e.g. `{host: "https://railsdesigner.com/"}`
24
+ # config.default_url_options = {}
25
+
26
+ # Generate text version from HTML content
27
+ # Default: `false`
28
+ # config.auto_generate_text = false
29
+
30
+ # Location for generated Courrier Emails
31
+ # Default: `app/emails`
32
+ # config.email_path = ""
33
+
34
+
35
+ # Select logger for the `logger` provider
36
+ # Default: `::Logger.new($stdout)` - Ruby's built-in Logger
37
+ # config.logger = ""
38
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: courrier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Rails Designer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-05-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: launchy
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4'
33
+ - !ruby/object:Gem::Dependency
34
+ name: nokogiri
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.18'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '2'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '1.18'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '2'
53
+ description: Modern, API-powered email delivery for Ruby apps with support for Postmark,
54
+ SendGrid, Mailgun and more.
55
+ email:
56
+ - devs@railsdesigner.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - Gemfile
62
+ - Gemfile.lock
63
+ - README.md
64
+ - Rakefile
65
+ - bin/console
66
+ - bin/release
67
+ - bin/setup
68
+ - courrier.gemspec
69
+ - lib/courrier.rb
70
+ - lib/courrier/configuration.rb
71
+ - lib/courrier/configuration/preview.rb
72
+ - lib/courrier/configuration/providers.rb
73
+ - lib/courrier/email.rb
74
+ - lib/courrier/email/address.rb
75
+ - lib/courrier/email/layouts.rb
76
+ - lib/courrier/email/options.rb
77
+ - lib/courrier/email/provider.rb
78
+ - lib/courrier/email/providers/base.rb
79
+ - lib/courrier/email/providers/logger.rb
80
+ - lib/courrier/email/providers/loops.rb
81
+ - lib/courrier/email/providers/mailgun.rb
82
+ - lib/courrier/email/providers/mailjet.rb
83
+ - lib/courrier/email/providers/mailpace.rb
84
+ - lib/courrier/email/providers/postmark.rb
85
+ - lib/courrier/email/providers/preview.rb
86
+ - lib/courrier/email/providers/preview/default.html.erb
87
+ - lib/courrier/email/providers/sendgrid.rb
88
+ - lib/courrier/email/providers/sparkpost.rb
89
+ - lib/courrier/email/request.rb
90
+ - lib/courrier/email/result.rb
91
+ - lib/courrier/email/transformer.rb
92
+ - lib/courrier/errors.rb
93
+ - lib/courrier/railtie.rb
94
+ - lib/courrier/tasks/courrier.rake
95
+ - lib/courrier/version.rb
96
+ - lib/generators/courrier/email_generator.rb
97
+ - lib/generators/courrier/install_generator.rb
98
+ - lib/generators/courrier/templates/email.rb.tt
99
+ - lib/generators/courrier/templates/initializer.rb.tt
100
+ homepage: https://railsdesigner.com/courrier/
101
+ licenses:
102
+ - MIT
103
+ metadata:
104
+ homepage_uri: https://railsdesigner.com/courrier/
105
+ source_code_uri: https://github.com/Rails-Designer/courrier/
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: 3.2.0
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 3.4.1
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Modern, API-powered email delivery for Rails apps
125
+ test_files: []