rails_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.
@@ -0,0 +1,127 @@
1
+ <!--
2
+ {
3
+ "to": "<%= email %>",
4
+ "subject": "<%= @options.subject %>"
5
+ }
6
+ -->
7
+
8
+ <!DOCTYPE html>
9
+ <html>
10
+ <head>
11
+ <meta charset="UTF-8">
12
+ <title><%= @options.subject %></title>
13
+ <style>
14
+ *, *::before, *::after { box-sizing: border-box; }
15
+ * { margin: 0; padding: 0; }
16
+ body { margin: 0; padding: 0; line-height: 1.5; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; background-color: rgb(241 245 249); }
17
+ pre { margin: 0; white-space: pre-wrap; }
18
+ .email {
19
+ width: 100%;
20
+ max-width: 65rem;
21
+ margin-left: auto; margin-right: auto;
22
+ background-color: #fff;
23
+ box-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
24
+ border-bottom-right-radius: 1rem; border-bottom-left-radius: 1rem;
25
+ }
26
+
27
+ .email__header { display: flex; align-items: center; padding: .75rem; -moz-column-gap: .75rem; column-gap: .75rem; }
28
+
29
+ .email__avatar {
30
+ flex-shrink: 0;
31
+ padding: .25rem;
32
+ width: 2rem; height: 2rem;
33
+ background-color: rgb(203 213 225);
34
+ color: rgb(51 65 85);
35
+ border-radius: 9999px;
36
+ }
37
+
38
+ .email__recipient { flex-grow: 1; font-size: .875rem; line-height: 1.25rem; }
39
+
40
+ .email__recipient-name { font-weight: 600; color: rgb(51 65 85); }
41
+
42
+ .email__metadata { display: flex; align-items: center; justify-content: space-between; }
43
+
44
+ .email__recipient-email { color: rgb(100 116 139); }
45
+
46
+ .email__datetime { font-size: .75rem; line-height: 1rem; color: rgb(148 163 184); }
47
+
48
+ .email__subject {
49
+ margin-left: 3.5rem;
50
+ font-size: 1.25rem; line-height: 1.75rem;
51
+ font-weight: 700;
52
+ letter-spacing: -.025em;
53
+ color: rgb(30 41 59)
54
+ }
55
+
56
+ .email__preview { margin-top: .75rem; margin-left: 3.5rem; padding-bottom: 1rem; }
57
+
58
+ .preview-toggle { display: none; }
59
+ .preview-toggle-label {
60
+ display: inline-block;
61
+ padding: .25rem .875rem;
62
+ font-size: .75rem; line-height: 1rem;
63
+ font-weight: 600;
64
+ color: rgb(30 41 59);
65
+ background-color: rgb(255 255 255);
66
+ border: 1px solid rgb(226 232 240);
67
+ border-radius: .375rem;
68
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
69
+ cursor: pointer;
70
+
71
+ &:hover { border-color: rgb(203 213 225); }
72
+ &:active { transform: scale(.98); }
73
+ }
74
+ .preview-toggle[disabled] + .preview-toggle-label { opacity: 0.5; cursor: not-allowed; }
75
+
76
+ .email__preview-html, .email__preview-text { margin-top: 1.5rem;}
77
+ .email__preview-html { display: none; }
78
+ .preview-toggle:checked ~ .email__preview-text { display: none; }
79
+ .preview-toggle:checked ~ .email__preview-html { display: block; }
80
+ </style>
81
+ </head>
82
+
83
+ <body>
84
+ <article class="email">
85
+ <header class="email__header">
86
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" data-slot="icon" class="email__avatar">
87
+ <path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd"/>
88
+ </svg>
89
+
90
+ <div class="email__recipient">
91
+ <% if name %>
92
+ <p class="email__recipient-name">
93
+ <%= name %>
94
+ </p>
95
+ <% end %>
96
+
97
+ <div class="email__metadata">
98
+ <p class="email__recipient-email">
99
+ <%= email %>
100
+ </p>
101
+
102
+ <time class="email__datetime" datetime="#{Time.now.strftime("%Y-%m-%d %H:%M:%S %z")}">
103
+ <%= Time.now.strftime("%Y-%m-%d %H:%M:%S %z") %>
104
+ </time>
105
+ </div>
106
+ </div>
107
+ </header>
108
+
109
+ <p class="email__subject">
110
+ <%= @options.subject %>
111
+ </p>
112
+
113
+ <div class="email__preview">
114
+ <input type="checkbox" id="preview-toggle" class="preview-toggle" <%= html ? "" : "disabled" %>>
115
+ <label for="preview-toggle" class="preview-toggle-label">Toggle HTML/Text</label>
116
+
117
+ <div class="email__preview-text">
118
+ <pre><%= text || "(no text content)" %></pre>
119
+ </div>
120
+
121
+ <div class="email__preview-html">
122
+ <%= html || "<p>(no HTML content)</p>" %>
123
+ </div>
124
+ </div>
125
+ </article>
126
+ </body>
127
+ </html>
@@ -0,0 +1,83 @@
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 Inbox < 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) if config.auto_open
19
+
20
+ "📮 Email saved to #{file_path} and #{email_destination}"
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(URL_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.inbox
48
+
49
+ def email_destination
50
+ return "opened in your default browser" if config.auto_open
51
+
52
+ path = begin
53
+ Rails.application.routes.url_helpers.courrier_path
54
+ rescue
55
+ "/courrier/ (Note: Add `mount Courrier::Engine => \"/courrier\"` to your routes.rb to enable routing)"
56
+ end
57
+
58
+ "available at #{path}"
59
+ end
60
+
61
+ URL_PARSER = (
62
+ defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER
63
+ )
64
+
65
+ class Email < Data.define(:path, :filename, :metadata)
66
+ Metadata = Data.define(:to, :subject)
67
+
68
+ def self.from_file(path)
69
+ content = File.read(path)
70
+ json = content[/<!--\s*(.*?)\s*-->/m, 1]
71
+ metadata = JSON.parse(json, symbolize_names: true)
72
+
73
+ new(
74
+ path: path,
75
+ filename: File.basename(path),
76
+ metadata: Metadata.new(**metadata)
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Courrier
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Courrier
6
+ end
7
+ end
@@ -0,0 +1,49 @@
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
+
6
+ if Courrier.configuration.email_path == File.join("courrier", "emails")
7
+ Courrier.configuration.email_path = Rails.root.join("app", "emails").to_s
8
+ end
9
+ end
10
+
11
+ ActiveSupport.on_load(:action_view) do
12
+ include Courrier::Email::Address
13
+ end
14
+
15
+ ActiveSupport.on_load(:action_controller) do
16
+ include Courrier::Email::Address
17
+ end
18
+
19
+ ActiveSupport.on_load(:active_job) do
20
+ include Courrier::Email::Address
21
+ end
22
+
23
+ rake_tasks do
24
+ load File.join(File.dirname(__FILE__), "tasks", "courrier.rake")
25
+ end
26
+
27
+ module ClassMethods
28
+ def deliver_later(**options)
29
+ job = Courrier::Email::DeliveryJob
30
+ job = job.set(**queue_options) if queue_options.any?
31
+
32
+ job.perform_later(name, **options)
33
+ end
34
+ end
35
+
36
+ module InheritedHook
37
+ def inherited(subclass)
38
+ super
39
+
40
+ if Rails.respond_to?(:application) && Rails.application
41
+ subclass.include Rails.application.routes.url_helpers
42
+ end
43
+ end
44
+ end
45
+
46
+ Courrier::Email.extend ClassMethods
47
+ Courrier::Email.singleton_class.prepend InheritedHook
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ namespace :tmp do
2
+ task :courrier do
3
+ rm_rf Dir["#{Courrier.configuration.inbox.destination}/[^.]*"], verbose: false
4
+ end
5
+
6
+ task clear: :courrier
7
+ end
8
+
9
+ namespace :courrier do
10
+ desc "Clear email files from `#{Courrier.configuration.inbox.destination}`"
11
+
12
+ task clear: "tmp:courrier"
13
+ end
@@ -0,0 +1,42 @@
1
+ module Courrier
2
+ class EmailGenerator < Rails::Generators::NamedBase
3
+ AVAILABLE_TEMPLATES = %w[welcome password_reset]
4
+
5
+ desc "Create a new Courrier Email class"
6
+
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ check_class_collision suffix: "Email"
10
+
11
+ class_option :skip_suffix, type: :boolean, default: false
12
+ class_option :template, type: :string, desc: "Template type (#{AVAILABLE_TEMPLATES.join(", ")})"
13
+
14
+ def copy_mailer_file
15
+ template template_file, destination_path
16
+ end
17
+
18
+ private
19
+
20
+ def file_name = super.delete_suffix("_email")
21
+
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}#{"_email" unless options[:skip_suffix]}.rb")
34
+ end
35
+
36
+ def template_exists?(path)
37
+ find_in_source_paths(path)
38
+ rescue
39
+ nil
40
+ end
41
+ end
42
+ 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,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
@@ -0,0 +1,19 @@
1
+ class <%= class_name %><%= options[:skip_suffix] ? "" : "Email" %> < <%= parent_class %>
2
+ def subject = ""
3
+
4
+ def text
5
+ <<~TEXT
6
+ TEXT
7
+ end
8
+
9
+ def html
10
+ <<~HTML
11
+ HTML
12
+ end
13
+
14
+ # def markdown
15
+ # # Requires: redcarpet, kramdown or commonmarker gem
16
+ # <<~MARKDOWN
17
+ # MARKDOWN
18
+ # end
19
+ end
@@ -0,0 +1,44 @@
1
+ Courrier.configure do |config|
2
+ include Courrier::Email::Address
3
+
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
+
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
+
39
+
40
+ # Set your marketing email provider
41
+ # config.subscriber = {
42
+ # provider: ""
43
+ # }
44
+ end
@@ -0,0 +1,3 @@
1
+ module RailsCourrier
2
+ VERSION = "0.5.0"
3
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_job"
5
+ require "action_view"
6
+ require "action_pack"
7
+ require "action_dispatch"
8
+ require "rails/railtie"
9
+ require "rails/engine"
10
+
11
+ require "launchy"
12
+
13
+ require "courrier"
14
+ require "courrier/engine"
15
+ require "courrier/railtie"
16
+ require "courrier/email/delivery_job"
17
+ require "courrier/email/providers/inbox"
18
+ require "courrier/configuration/inbox"
19
+
20
+ Courrier::Email::Provider::PROVIDERS[:inbox] = Courrier::Email::Providers::Inbox
21
+
22
+ module RailsCourrier
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rails_courrier/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rails_courrier"
7
+ spec.version = RailsCourrier::VERSION
8
+ spec.authors = ["Rails Designer"]
9
+ spec.email = ["devs@railsdesigner.com"]
10
+
11
+ spec.summary = "Rails integration for Courrier email delivery"
12
+ spec.description = "Rails engine, generators, ActiveJob support, inbox previews and rake tasks for Courrier."
13
+ spec.homepage = "https://railsdesigner.com/open-source/rails_courrier/"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/Rails-Designer/rails_courrier/"
18
+
19
+ spec.files = Dir["{bin,app,config,lib}/**/*", "Rakefile", "README.md", "rails_courrier.gemspec", "Gemfile", "Gemfile.lock"]
20
+
21
+ spec.required_ruby_version = ">= 4.0.0"
22
+
23
+ spec.add_dependency "courrier", "~> 0.11.0"
24
+ spec.add_dependency "launchy", ">= 3.1", "< 4"
25
+ spec.add_dependency "railties", ">= 7.0"
26
+ spec.add_dependency "actionpack", ">= 7.0"
27
+ spec.add_dependency "activejob", ">= 7.0"
28
+ spec.add_dependency "actionview", ">= 7.0"
29
+ end