goodmail 0.1.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 +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +166 -0
- data/Rakefile +4 -0
- data/lib/goodmail/builder.rb +118 -0
- data/lib/goodmail/configuration.rb +50 -0
- data/lib/goodmail/dispatcher.rb +58 -0
- data/lib/goodmail/error.rb +7 -0
- data/lib/goodmail/layout.erb +235 -0
- data/lib/goodmail/layout.rb +40 -0
- data/lib/goodmail/mailer.rb +52 -0
- data/lib/goodmail/version.rb +5 -0
- data/lib/goodmail.rb +49 -0
- data/sig/goodmail.rbs +4 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fa74c986faf7a129fa8daa4546192918312488290ad46aff8f889009efdefbe4
|
4
|
+
data.tar.gz: 9d138231f61b68a137a2726b989788bd180d7c15e6fa6a4f414c929243c8b343
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 66d62b19ff6baebd3512bc3d04f60b0b329fec2e36b90ccd8cc9701a955206b91f73f86af4ccdb9a74501f0e05d4f36c948d1683b54049fdebf821244965331c
|
7
|
+
data.tar.gz: a1c74368751fc8c5701f6aefc588a118ac1839691d5657475024343e0b007646271e9ffe1b7c23294245dfaedde263e421a4b24418a3df0fc74998a983f49ab3
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Javi R
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
# 💌 Goodmail - Make your transactional emails look beautiful
|
2
|
+
[](https://badge.fury.io/rb/pay)
|
3
|
+
|
4
|
+
Send beautiful, simple transactional emails with zero HTML hell.
|
5
|
+
|
6
|
+
Goodmail turns your ugly, default, text-only emails into SaaS-ready emails. It's an opinionated, minimal, expressive Ruby DSL for sending beautiful, production-grade transactional emails in Rails apps — no templates, no partials, no HTML hell. The template works well and looks nice across email clients.
|
7
|
+
|
8
|
+
Here's the catch: there's only one template. You can't change it. You're guaranteed you'll send good emails, but the cost is you don't have much flexibility. If you're okay with this, welcome to `goodmail`! You'll be shipping decent emails that look great everywhere in no time.
|
9
|
+
|
10
|
+
(And you can still use Action Mailer for all other template-intensive emails – Goodmail doesn't replace Action Mailer, just builds on top of it!)
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem "goodmail"
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
```bash
|
23
|
+
bundle install
|
24
|
+
```
|
25
|
+
|
26
|
+
## Configuration
|
27
|
+
|
28
|
+
You can (and should!) edit the default strings in the Goodmail initializer.
|
29
|
+
|
30
|
+
Create an initializer file at `config/initializers/goodmail.rb`:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
# config/initializers/goodmail.rb
|
34
|
+
|
35
|
+
Goodmail.configure do |config|
|
36
|
+
# The main accent color used for buttons and links in the email body.
|
37
|
+
# Default: "#348eda"
|
38
|
+
config.brand_color = "#E62F17" # Your brand's primary color
|
39
|
+
|
40
|
+
# The company name displayed in the email footer and used by the default `sign` helper.
|
41
|
+
# Default: "Example Inc."
|
42
|
+
config.company_name = "MyApp Inc."
|
43
|
+
|
44
|
+
# Optional: URL to your company logo. If set, it will appear centered in the header.
|
45
|
+
# Recommended size: max-height 30px.
|
46
|
+
# Default: nil
|
47
|
+
config.logo_url = "https://cdn.myapp.com/images/email_logo.png"
|
48
|
+
|
49
|
+
# Optional: Custom text displayed in the footer below the copyright.
|
50
|
+
# Use this to explain why the user received the email.
|
51
|
+
# Default: nil
|
52
|
+
config.footer_text = "You are receiving this email because you signed up for an account at MyApp."
|
53
|
+
|
54
|
+
# Optional: Global default URL for unsubscribe links (both the List-Unsubscribe
|
55
|
+
# header and the optional visible link in the footer).
|
56
|
+
# Goodmail *does not* handle the unsubscribe logic; you must provide a valid URL.
|
57
|
+
# Can be overridden per email via headers[:unsubscribe_url].
|
58
|
+
# Default: nil
|
59
|
+
config.unsubscribe_url = "https://myapp.com/emails/unsubscribe"
|
60
|
+
|
61
|
+
# Optional: Whether to show a visible unsubscribe link in the footer.
|
62
|
+
# Requires an unsubscribe URL to be set (globally or per-email).
|
63
|
+
# Default: false
|
64
|
+
config.show_footer_unsubscribe_link = true
|
65
|
+
|
66
|
+
# Optional: The text for the visible footer unsubscribe link.
|
67
|
+
# Default: "Unsubscribe"
|
68
|
+
config.footer_unsubscribe_link_text = "Click here to unsubscribe"
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
Make sure to restart your Rails server after creating or modifying the initializer.
|
73
|
+
|
74
|
+
## Quick start
|
75
|
+
|
76
|
+
Use the `Goodmail.compose` method to compose emails using the DSL, then call `.deliver_now` or `.deliver_later` on it (your usual standard Action Mailer methods)
|
77
|
+
|
78
|
+
### Basic Example
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# In a controller action, background job, or service object
|
82
|
+
Goodmail.compose(to: user.email, subject: "Welcome!") do
|
83
|
+
greeting "Hey #{user.first_name},"
|
84
|
+
text "Thanks for joining. Confirm below:"
|
85
|
+
button "Confirm account", confirm_url
|
86
|
+
end.deliver_now
|
87
|
+
```
|
88
|
+
|
89
|
+
### Deliver Later (Background Job)
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
mail = Goodmail.compose(
|
93
|
+
to: @user.email,
|
94
|
+
from: ..., # etc.
|
95
|
+
subject: "Your password has been reset"
|
96
|
+
) do
|
97
|
+
# ... DSL content ...
|
98
|
+
end
|
99
|
+
|
100
|
+
mail.deliver_later
|
101
|
+
```
|
102
|
+
|
103
|
+
*(Requires Active Job configured.)*
|
104
|
+
|
105
|
+
### Available DSL Methods
|
106
|
+
|
107
|
+
Inside the `Goodmail.compose` block, you have access to these methods:
|
108
|
+
|
109
|
+
* `h1(text)`, `h2(text)`, `h3(text)`: Styled heading tags.
|
110
|
+
* `text(string)`: A paragraph of text. Handles `\n` for line breaks. Allows simple inline `<a>` tags with `href` attributes; other HTML is stripped for safety.
|
111
|
+
* `button(link_text, url)`: A prominent, styled call-to-action button.
|
112
|
+
* `image(src, alt = "", width: nil, height: nil)`: Embeds an image, centered by default.
|
113
|
+
* `space(pixels = 16)`: Adds vertical whitespace.
|
114
|
+
* `line`: Adds a horizontal rule (`<hr>`).
|
115
|
+
* `center { ... }`: Centers the content generated within the block.
|
116
|
+
* `sign(name = Goodmail.config.company_name)`: Adds a standard closing signature line.
|
117
|
+
* `html(raw_html_string)`: **Use with extreme caution.** Allows embedding raw, *un-sanitized* HTML. Only use this if you absolutely trust the source of the string.
|
118
|
+
|
119
|
+
### Adding Unsubscribe Functionality
|
120
|
+
|
121
|
+
Goodmail helps you add the `List-Unsubscribe` header and an optional visible link, but **you must provide the actual URL** where users can unsubscribe. Goodmail does not generate unsubscribe URLs or handle the logic.
|
122
|
+
|
123
|
+
1. **Provide the URL:** You have two options:
|
124
|
+
* **Globally:** Set `config.unsubscribe_url = "your_global_url"` in the initializer (`config/initializers/goodmail.rb`).
|
125
|
+
* **Per-Email:** Pass `unsubscribe_url: "your_specific_url"` in the headers hash when calling `Goodmail.compose`. This overrides the global setting for that email.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
# Example using per-email override
|
129
|
+
mail = Goodmail.compose(
|
130
|
+
to: recipient.email,
|
131
|
+
# ... other headers ...
|
132
|
+
unsubscribe_url: manage_subscription_url(recipient) # Your app's URL helper
|
133
|
+
) do
|
134
|
+
# ... email content ...
|
135
|
+
end
|
136
|
+
```
|
137
|
+
*If an `unsubscribe_url` is provided (either globally or per-email), Goodmail will automatically add the standard `List-Unsubscribe` header.*
|
138
|
+
|
139
|
+
2. **Optionally Show Footer Link:**
|
140
|
+
* To show a visible link in the email footer, set `config.show_footer_unsubscribe_link = true` in the initializer.
|
141
|
+
* You can customize the link text with `config.footer_unsubscribe_link_text` (default: "Unsubscribe").
|
142
|
+
* *Note: The footer link only appears if an `unsubscribe_url` was provided (step 1) AND `config.show_footer_unsubscribe_link` is true.*
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
# config/initializers/goodmail.rb
|
146
|
+
Goodmail.configure do |config|
|
147
|
+
# ... other settings
|
148
|
+
config.unsubscribe_url = "https://myapp.com/preferences"
|
149
|
+
config.show_footer_unsubscribe_link = true
|
150
|
+
config.footer_unsubscribe_link_text = "Manage email preferences"
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
## Development
|
155
|
+
|
156
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
157
|
+
|
158
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
159
|
+
|
160
|
+
## Contributing
|
161
|
+
|
162
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/goodmail. Our code of conduct is: just be nice and make your mom proud of what you do and post online.
|
163
|
+
|
164
|
+
## License
|
165
|
+
|
166
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "erb"
|
3
|
+
require "rails-html-sanitizer" # Require the sanitizer
|
4
|
+
|
5
|
+
module Goodmail
|
6
|
+
# Builds the HTML content string based on DSL method calls.
|
7
|
+
class Builder
|
8
|
+
include ERB::Util # For the h() helper
|
9
|
+
|
10
|
+
# Initialize a basic sanitizer allowing only <a> tags with href
|
11
|
+
HTML_SANITIZER = Rails::Html::SafeListSanitizer.new
|
12
|
+
ALLOWED_TAGS = %w(a).freeze
|
13
|
+
ALLOWED_ATTRIBUTES = %w(href).freeze
|
14
|
+
|
15
|
+
attr_reader :parts
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@parts = []
|
19
|
+
end
|
20
|
+
|
21
|
+
# DSL Methods
|
22
|
+
|
23
|
+
# Adds a paragraph of text. Handles newline characters for <br> tags.
|
24
|
+
# Allows safe inline <a> tags with href attributes; strips other HTML.
|
25
|
+
def text(str)
|
26
|
+
# Sanitize first, allowing only safe tags like <a>
|
27
|
+
sanitized_content = HTML_SANITIZER.sanitize(
|
28
|
+
str.to_s, # Ensure input is a string
|
29
|
+
tags: ALLOWED_TAGS,
|
30
|
+
attributes: ALLOWED_ATTRIBUTES
|
31
|
+
)
|
32
|
+
# Then handle newlines and wrap in paragraph
|
33
|
+
parts << tag(:p, sanitized_content.gsub(/\n/, "<br>"), style: "margin:16px 0; line-height: 1.6;")
|
34
|
+
end
|
35
|
+
|
36
|
+
def button(text, url)
|
37
|
+
# Use a class for easier styling via layout CSS
|
38
|
+
parts << %(<div class="goodmail-button" style="text-align: center; margin: 24px 0;"><a href="#{h url}"><span style=\"color:#ffffff;\">#{h text}</span></a></div>)
|
39
|
+
end
|
40
|
+
|
41
|
+
def image(src, alt = "", width: nil, height: nil)
|
42
|
+
style = "max-width:100%; height:auto;"
|
43
|
+
style += " width:#{width}px;" if width
|
44
|
+
style += " height:#{height}px;" if height
|
45
|
+
# Use a class for easier styling via layout CSS
|
46
|
+
parts << %(<img class="goodmail-image" src="#{h src}" alt="#{h alt}" style="#{style}">)
|
47
|
+
end
|
48
|
+
|
49
|
+
def space(px = 16)
|
50
|
+
# Rely on CSS height for spacing, avoid if possible
|
51
|
+
parts << %(<div style="height:#{Integer(px)}px; line-height: #{Integer(px)}px; font-size: 1px;"></div>)
|
52
|
+
end
|
53
|
+
|
54
|
+
def sign(name = Goodmail.config.company_name)
|
55
|
+
# Directly add the paragraph with the raw styled span
|
56
|
+
# Name is escaped using h() to prevent injection if config is compromised
|
57
|
+
parts << %(<p style="margin:16px 0; line-height: 1.6;"><span style="color: #888;">– #{h name}</span></p>)
|
58
|
+
end
|
59
|
+
|
60
|
+
%i[h1 h2 h3].each do |heading_tag|
|
61
|
+
define_method(heading_tag) do |str|
|
62
|
+
# Added basic heading styles, consistent with layout.erb
|
63
|
+
style = case heading_tag
|
64
|
+
when :h1 then "margin: 40px 0 10px; font-size: 32px; font-weight: 500; line-height: 1.2em;"
|
65
|
+
when :h2 then "margin: 40px 0 10px; font-size: 24px; font-weight: 400; line-height: 1.2em;"
|
66
|
+
when :h3 then "margin: 40px 0 10px; font-size: 18px; font-weight: 400; line-height: 1.2em;"
|
67
|
+
else "margin: 16px 0; line-height: 1.6;"
|
68
|
+
end
|
69
|
+
# Headings should still have their content escaped
|
70
|
+
parts << tag(heading_tag, h(str), style: style)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def center(&block)
|
75
|
+
wrap("div", "text-align:center;", &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
def line
|
79
|
+
# Use a class for easier styling via layout CSS
|
80
|
+
parts << %(<hr class="goodmail-hr">)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Allows inserting raw, *trusted* HTML. Use with extreme caution.
|
84
|
+
def html(raw_html_string)
|
85
|
+
parts << raw_html_string.to_s
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the collected HTML parts joined together.
|
89
|
+
def html_output
|
90
|
+
parts.join("\n")
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Helper for creating simple HTML tags with optional style
|
96
|
+
# Assumes content is already appropriately escaped or marked safe.
|
97
|
+
def tag(name, content, style: nil)
|
98
|
+
style_attr = style ? " style=\"#{h style}\"" : ""
|
99
|
+
"<#{name}#{style_attr}>#{content}</#{name}>"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Temporarily captures parts generated within a block into a wrapped tag.
|
103
|
+
def wrap(tag_name, style, &block)
|
104
|
+
original_parts = @parts
|
105
|
+
@parts = []
|
106
|
+
yield # Execute the block, collecting parts into the temporary @parts
|
107
|
+
inner_html = @parts.join("\n")
|
108
|
+
@parts = original_parts # Restore original parts array
|
109
|
+
@parts << tag(tag_name, inner_html, style: style)
|
110
|
+
ensure
|
111
|
+
# Ensure parts are restored even if the block raises an error
|
112
|
+
@parts = original_parts if defined?(original_parts)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Prevent external modification of the parts array directly
|
116
|
+
attr_writer :parts
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "ostruct"
|
3
|
+
|
4
|
+
module Goodmail
|
5
|
+
# Handles configuration settings for the Goodmail gem.
|
6
|
+
module Configuration
|
7
|
+
# Default configuration values
|
8
|
+
DEFAULT_CONFIG = OpenStruct.new(
|
9
|
+
brand_color: "#348eda",
|
10
|
+
company_name: "Example Inc.",
|
11
|
+
logo_url: nil,
|
12
|
+
# Optional footer text (e.g., "Why you received this email")
|
13
|
+
footer_text: nil,
|
14
|
+
# Optional: Global default unsubscribe URL.
|
15
|
+
# Can be overridden per email via headers[:unsubscribe_url].
|
16
|
+
# User is responsible for providing a valid URL to manage email subscriptions.
|
17
|
+
unsubscribe_url: nil,
|
18
|
+
# Show a visible unsubscribe link in the footer?
|
19
|
+
show_footer_unsubscribe_link: false,
|
20
|
+
# Text for the footer unsubscribe link
|
21
|
+
footer_unsubscribe_link_text: "Unsubscribe"
|
22
|
+
).freeze # Freeze the default object to prevent accidental modification
|
23
|
+
|
24
|
+
# Provides the configuration block helper.
|
25
|
+
# Allows users to modify the configuration in an initializer:
|
26
|
+
# Goodmail.configure do |config|
|
27
|
+
# config.brand_color = "#ff0000"
|
28
|
+
# end
|
29
|
+
def configure
|
30
|
+
# Ensure config is initialized before yielding
|
31
|
+
yield config
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the current configuration object.
|
35
|
+
# Initializes with a copy of the defaults if not already configured.
|
36
|
+
def config
|
37
|
+
# Use defined? check for more robust initialization in edge cases
|
38
|
+
@config = DEFAULT_CONFIG.dup unless defined?(@config) && @config
|
39
|
+
@config
|
40
|
+
end
|
41
|
+
|
42
|
+
# Resets the configuration back to the default values.
|
43
|
+
# Primarily useful for testing environments.
|
44
|
+
def reset_config!
|
45
|
+
@config = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Removed attr_reader/writer - relying on explicit config method.
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "action_mailer"
|
3
|
+
require "cgi" # For unescaping HTML in plaintext generation
|
4
|
+
require_relative "mailer" # Require the internal mailer
|
5
|
+
|
6
|
+
module Goodmail
|
7
|
+
# Responsible for orchestrating the building of the Mail::Message object.
|
8
|
+
module Dispatcher
|
9
|
+
extend self
|
10
|
+
|
11
|
+
# Builds the Mail::Message object with HTML and Text parts, wrapped in
|
12
|
+
# an ActionMailer::MessageDelivery object.
|
13
|
+
# @api private
|
14
|
+
def build_message(headers, &block)
|
15
|
+
# 1. Initialize the Builder
|
16
|
+
builder = Goodmail::Builder.new
|
17
|
+
|
18
|
+
# 2. Execute the DSL block within the Builder instance
|
19
|
+
builder.instance_eval(&block) if block_given?
|
20
|
+
|
21
|
+
# 3. Determine the final unsubscribe URL (user-provided)
|
22
|
+
unsubscribe_url = headers[:unsubscribe_url] || Goodmail.config.unsubscribe_url
|
23
|
+
|
24
|
+
# 4. Render the raw HTML body using the Layout
|
25
|
+
# This raw HTML still has the <style> block needed by Premailer.
|
26
|
+
raw_html_body = Goodmail::Layout.render(
|
27
|
+
builder.html_output,
|
28
|
+
headers[:subject],
|
29
|
+
unsubscribe_url: unsubscribe_url
|
30
|
+
)
|
31
|
+
|
32
|
+
# 5. Slice standard headers for the mailer action
|
33
|
+
mailer_headers = slice_mail_headers(headers)
|
34
|
+
|
35
|
+
# 6. Build the mail object via the internal Mailer class action.
|
36
|
+
# Pass the raw HTML (for Premailer) and unsubscribe URL.
|
37
|
+
# Plaintext is now generated by Premailer inside the mailer action.
|
38
|
+
delivery_object = Goodmail::Mailer.compose_message(
|
39
|
+
mailer_headers,
|
40
|
+
raw_html_body,
|
41
|
+
nil, # Pass nil for raw_text_body - Premailer generates it
|
42
|
+
unsubscribe_url
|
43
|
+
)
|
44
|
+
|
45
|
+
# 7. Return the ActionMailer::MessageDelivery object
|
46
|
+
delivery_object
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Whitelist standard headers to pass to ActionMailer's mail() method
|
52
|
+
def slice_mail_headers(h)
|
53
|
+
h.slice(:to, :from, :cc, :bcc, :reply_to, :subject)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Removed generate_plaintext - now handled by Premailer in Mailer#compose_message
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
3
|
+
<head>
|
4
|
+
<meta name="viewport" content="width=device-width" />
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
6
|
+
<title><%= subject %></title>
|
7
|
+
<style type="text/css">
|
8
|
+
/* Base Styles */
|
9
|
+
img {
|
10
|
+
max-width: 100%;
|
11
|
+
}
|
12
|
+
body {
|
13
|
+
-webkit-font-smoothing: antialiased;
|
14
|
+
-webkit-text-size-adjust: none;
|
15
|
+
width: 100% !important;
|
16
|
+
height: 100%;
|
17
|
+
line-height: 1.6em;
|
18
|
+
background-color: #f6f6f6; /* Body background */
|
19
|
+
margin: 0;
|
20
|
+
padding: 0;
|
21
|
+
}
|
22
|
+
table td {
|
23
|
+
vertical-align: top;
|
24
|
+
}
|
25
|
+
.body-wrap {
|
26
|
+
background-color: #f6f6f6;
|
27
|
+
width: 100%;
|
28
|
+
}
|
29
|
+
.container {
|
30
|
+
display: block !important;
|
31
|
+
max-width: 600px !important;
|
32
|
+
margin: 0 auto !important; /* Center the container */
|
33
|
+
clear: both !important;
|
34
|
+
}
|
35
|
+
.content {
|
36
|
+
max-width: 600px;
|
37
|
+
margin: 0 auto;
|
38
|
+
display: block;
|
39
|
+
padding: 20px;
|
40
|
+
}
|
41
|
+
.main {
|
42
|
+
background-color: #ffffff; /* Email body background */
|
43
|
+
border: 1px solid #e9e9e9;
|
44
|
+
border-radius: 3px;
|
45
|
+
}
|
46
|
+
.content-wrap {
|
47
|
+
padding: 30px; /* Padding inside the white content area */
|
48
|
+
}
|
49
|
+
.header {
|
50
|
+
width: 100%;
|
51
|
+
margin-bottom: 20px;
|
52
|
+
text-align: left;
|
53
|
+
}
|
54
|
+
.footer {
|
55
|
+
width: 100%;
|
56
|
+
clear: both;
|
57
|
+
color: #999;
|
58
|
+
padding: 20px;
|
59
|
+
margin-top: 20px;
|
60
|
+
}
|
61
|
+
.footer p,
|
62
|
+
.footer a,
|
63
|
+
.footer td {
|
64
|
+
color: #999;
|
65
|
+
font-size: 12px;
|
66
|
+
text-align: center;
|
67
|
+
padding: 0 0 10px; /* Add padding between footer lines */
|
68
|
+
}
|
69
|
+
.footer a {
|
70
|
+
text-decoration: underline;
|
71
|
+
color: #999; /* Keep footer links grey */
|
72
|
+
}
|
73
|
+
|
74
|
+
/* Typography */
|
75
|
+
h1, h2, h3 {
|
76
|
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
77
|
+
color: #000000;
|
78
|
+
margin: 40px 0 10px;
|
79
|
+
line-height: 1.2em;
|
80
|
+
font-weight: 400;
|
81
|
+
}
|
82
|
+
h1 { font-size: 32px; font-weight: 500; }
|
83
|
+
h2 { font-size: 24px; }
|
84
|
+
h3 { font-size: 18px; }
|
85
|
+
p, ul, ol {
|
86
|
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
87
|
+
font-size: 15px; /* Slightly larger base font */
|
88
|
+
font-weight: normal;
|
89
|
+
margin-bottom: 15px;
|
90
|
+
line-height: 1.6;
|
91
|
+
}
|
92
|
+
a {
|
93
|
+
color: <%= config.brand_color %>; /* Use brand color for links */
|
94
|
+
text-decoration: underline;
|
95
|
+
}
|
96
|
+
|
97
|
+
/* Elements generated by Builder */
|
98
|
+
.goodmail-button a {
|
99
|
+
font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;
|
100
|
+
box-sizing: border-box;
|
101
|
+
font-size: 14px;
|
102
|
+
color: #FFF !important; /* Ensure button text is white */
|
103
|
+
text-decoration: none;
|
104
|
+
line-height: 2em;
|
105
|
+
font-weight: bold;
|
106
|
+
text-align: center;
|
107
|
+
cursor: pointer;
|
108
|
+
display: inline-block;
|
109
|
+
border-radius: 5px;
|
110
|
+
text-transform: capitalize;
|
111
|
+
background-color: <%= config.brand_color %>;
|
112
|
+
margin: 0;
|
113
|
+
border-color: <%= config.brand_color %>;
|
114
|
+
border-style: solid;
|
115
|
+
border-width: 10px 20px;
|
116
|
+
}
|
117
|
+
.goodmail-hr {
|
118
|
+
border: none;
|
119
|
+
border-top: 1px solid #e9e9e9; /* Match main border color */
|
120
|
+
margin: 30px 0;
|
121
|
+
}
|
122
|
+
.goodmail-image {
|
123
|
+
display: block;
|
124
|
+
margin: 20px auto; /* Center images added via image() */
|
125
|
+
max-width: 100%;
|
126
|
+
height: auto;
|
127
|
+
}
|
128
|
+
/* Add styles for headings from builder if needed, or rely on base H tags */
|
129
|
+
|
130
|
+
/* Responsive Styles */
|
131
|
+
@media only screen and (max-width: 640px) {
|
132
|
+
body {
|
133
|
+
padding: 0 !important;
|
134
|
+
}
|
135
|
+
h1, h2, h3 {
|
136
|
+
font-weight: 800 !important; margin: 20px 0 5px !important;
|
137
|
+
}
|
138
|
+
h1 { font-size: 22px !important; }
|
139
|
+
h2 { font-size: 18px !important; }
|
140
|
+
h3 { font-size: 16px !important; }
|
141
|
+
.container {
|
142
|
+
padding: 0 !important; width: 100% !important;
|
143
|
+
}
|
144
|
+
.content {
|
145
|
+
padding: 0 !important;
|
146
|
+
}
|
147
|
+
.content-wrap {
|
148
|
+
padding: 15px !important; /* Reduced padding on mobile */
|
149
|
+
}
|
150
|
+
.goodmail-button a {
|
151
|
+
padding: 8px 16px !important; /* Slightly smaller button padding */
|
152
|
+
font-size: 13px !important;
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
/* Outlook/MSO Specific Fixes */
|
157
|
+
/* Add any necessary MSO fixes here if testing reveals issues */
|
158
|
+
|
159
|
+
</style>
|
160
|
+
</head>
|
161
|
+
|
162
|
+
<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
|
163
|
+
|
164
|
+
<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
|
165
|
+
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
166
|
+
<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
|
167
|
+
<td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
|
168
|
+
<div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
|
169
|
+
|
170
|
+
<!-- Optional Header Section -->
|
171
|
+
<% if config.logo_url %>
|
172
|
+
<table class="header" width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0 0 20px; text-align: left;" align="left">
|
173
|
+
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
174
|
+
<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0;" valign="top">
|
175
|
+
<img src="<%= config.logo_url %>" alt="<%= config.company_name %> Logo" height="30" style="max-height: 30px; width: auto; border:0; outline:none; text-decoration:none; display:block;" />
|
176
|
+
</td>
|
177
|
+
</tr>
|
178
|
+
</table>
|
179
|
+
<% end %>
|
180
|
+
|
181
|
+
<!-- Main Content Body -->
|
182
|
+
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff">
|
183
|
+
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
184
|
+
<td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 30px;" valign="top">
|
185
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
186
|
+
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
187
|
+
<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top">
|
188
|
+
<!-- DSL Content Goes Here -->
|
189
|
+
<%= body_html %>
|
190
|
+
</td>
|
191
|
+
</tr>
|
192
|
+
</table>
|
193
|
+
</td>
|
194
|
+
</tr>
|
195
|
+
</table>
|
196
|
+
|
197
|
+
<!-- Footer Section -->
|
198
|
+
<div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 20px 0 0; padding: 20px;">
|
199
|
+
<table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
200
|
+
<!-- Copyright -->
|
201
|
+
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
202
|
+
<td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; text-align: center; margin: 0; padding: 0 0 10px; color: #999;" align="center" valign="top">
|
203
|
+
© <%= Date.today.year %> <%= config.company_name %>
|
204
|
+
</td>
|
205
|
+
</tr>
|
206
|
+
|
207
|
+
<!-- Optional Footer Text -->
|
208
|
+
<% if config.footer_text.present? %>
|
209
|
+
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
210
|
+
<td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; text-align: center; margin: 0; padding: 0 0 10px; color: #999;" align="center" valign="top">
|
211
|
+
<%= config.footer_text %>
|
212
|
+
</td>
|
213
|
+
</tr>
|
214
|
+
<% end %>
|
215
|
+
|
216
|
+
<!-- Optional Unsubscribe Link -->
|
217
|
+
<% if config.show_footer_unsubscribe_link && defined?(unsubscribe_url) && unsubscribe_url.present? %>
|
218
|
+
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
219
|
+
<td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; text-align: center; margin: 0; padding: 0 0 10px; color: #999;" align="center" valign="top">
|
220
|
+
<a href="<%= unsubscribe_url %>" style="color: #999;"><%= config.footer_unsubscribe_link_text %></a>
|
221
|
+
</td>
|
222
|
+
</tr>
|
223
|
+
<% end %>
|
224
|
+
|
225
|
+
</table>
|
226
|
+
</div>
|
227
|
+
|
228
|
+
</div> <!-- /.content -->
|
229
|
+
</td>
|
230
|
+
<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
|
231
|
+
</tr>
|
232
|
+
</table>
|
233
|
+
|
234
|
+
</body>
|
235
|
+
</html>
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "erb"
|
3
|
+
|
4
|
+
module Goodmail
|
5
|
+
# Handles rendering the final HTML email by injecting
|
6
|
+
# the built content into the main layout template.
|
7
|
+
module Layout
|
8
|
+
extend self
|
9
|
+
|
10
|
+
# Path to the default layout template within the gem
|
11
|
+
DEFAULT_LAYOUT_PATH = File.expand_path("layout.erb", __dir__)
|
12
|
+
|
13
|
+
# Renders the email content within the layout template.
|
14
|
+
#
|
15
|
+
# @param body_html [String] The HTML content generated by the Builder.
|
16
|
+
# @param subject [String] The email subject line (used for the <title> tag).
|
17
|
+
# @param layout_path [String, nil] Optional path to a custom layout ERB file.
|
18
|
+
# @param unsubscribe_url [String, nil] Optional URL for the footer unsubscribe link.
|
19
|
+
# @return [String] The full HTML document string.
|
20
|
+
def render(body_html, subject, layout_path: nil, unsubscribe_url: nil)
|
21
|
+
template_path = layout_path || DEFAULT_LAYOUT_PATH
|
22
|
+
|
23
|
+
unless File.exist?(template_path)
|
24
|
+
raise Goodmail::Error, "Layout template not found at #{template_path}"
|
25
|
+
end
|
26
|
+
|
27
|
+
template_content = File.read(template_path)
|
28
|
+
|
29
|
+
# Use ERB#result_with_hash for cleaner variable passing (Ruby 2.5+)
|
30
|
+
ERB.new(template_content).result_with_hash(
|
31
|
+
body_html: body_html,
|
32
|
+
subject: subject || "",
|
33
|
+
config: Goodmail.config, # Make config available
|
34
|
+
unsubscribe_url: unsubscribe_url # Pass unsubscribe URL to template
|
35
|
+
)
|
36
|
+
rescue => e
|
37
|
+
raise Goodmail::Error, "Failed to render layout template: #{e.message}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "action_mailer"
|
3
|
+
require "premailer" # Require premailer library
|
4
|
+
|
5
|
+
module Goodmail
|
6
|
+
# Internal Mailer class.
|
7
|
+
# Inherits from ActionMailer::Base to provide the necessary context
|
8
|
+
# for building Mail::Message objects, but without relying on any
|
9
|
+
# host application views or layouts.
|
10
|
+
class Mailer < ActionMailer::Base
|
11
|
+
# No explicit default settings needed here usually,
|
12
|
+
# as headers (:from, etc.) should be provided in Goodmail.compose
|
13
|
+
# It will, however, inherit default ActionMailer settings from the Rails app
|
14
|
+
# (like delivery_method, smtp_settings, default_url_options) which is good.
|
15
|
+
|
16
|
+
# This instance method acts as the mailer action.
|
17
|
+
# It's called via Goodmail::Mailer.compose_message(...)
|
18
|
+
# Action Mailer wraps the result in a MessageDelivery object.
|
19
|
+
# It uses Premailer to inline CSS and generate plaintext.
|
20
|
+
# @api internal
|
21
|
+
def compose_message(headers, raw_html_body, raw_text_body, unsubscribe_url)
|
22
|
+
# Initialize Premailer with the raw HTML body from the layout
|
23
|
+
premailer = Premailer.new(
|
24
|
+
raw_html_body,
|
25
|
+
with_html_string: true,
|
26
|
+
# Common options:
|
27
|
+
adapter: :nokogiri, # Faster parser
|
28
|
+
preserve_styles: true, # Keep <style> block for clients that support it
|
29
|
+
remove_ids: false, # Keep IDs if needed for anchors etc.
|
30
|
+
remove_comments: true
|
31
|
+
)
|
32
|
+
|
33
|
+
# Get processed content
|
34
|
+
inlined_html = premailer.to_inline_css
|
35
|
+
generated_plain_text = premailer.to_plain_text
|
36
|
+
|
37
|
+
# Add List-Unsubscribe header to the headers hash *before* calling mail()
|
38
|
+
if unsubscribe_url.is_a?(String) && !unsubscribe_url.strip.empty?
|
39
|
+
headers["List-Unsubscribe"] = "<#{unsubscribe_url.strip}>"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Call the instance-level `mail` method
|
43
|
+
mail(headers) do |format|
|
44
|
+
# Use the premailer-generated plaintext
|
45
|
+
format.text { render plain: generated_plain_text }
|
46
|
+
# Use the CSS-inlined HTML
|
47
|
+
format.html { render html: inlined_html.html_safe }
|
48
|
+
end
|
49
|
+
# Action Mailer automatically returns the MessageDelivery object
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/goodmail.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Order matters: Load dependencies and base modules first.
|
4
|
+
require "action_mailer"
|
5
|
+
require "ostruct"
|
6
|
+
|
7
|
+
# Load Goodmail components
|
8
|
+
require_relative "goodmail/version"
|
9
|
+
require_relative "goodmail/configuration"
|
10
|
+
require_relative "goodmail/error" # Load Error class explicitly if needed elsewhere
|
11
|
+
require_relative "goodmail/builder"
|
12
|
+
require_relative "goodmail/layout"
|
13
|
+
require_relative "goodmail/mailer" # Require the internal Mailer
|
14
|
+
require_relative "goodmail/dispatcher"
|
15
|
+
|
16
|
+
# The main namespace for the Goodmail gem.
|
17
|
+
# Provides configuration and the primary .compose method.
|
18
|
+
module Goodmail
|
19
|
+
# Extend self with Configuration module methods (config, configure, reset_config!)
|
20
|
+
extend Configuration
|
21
|
+
|
22
|
+
# Composes a Mail::Message object using the Goodmail DSL and layout.
|
23
|
+
#
|
24
|
+
# This is the primary entry point for creating emails with Goodmail.
|
25
|
+
# The returned Mail::Message object can then have `.deliver_now` or
|
26
|
+
# `.deliver_later` called on it.
|
27
|
+
#
|
28
|
+
# @param headers [Hash] Mail headers (:to, :from, :subject, etc.)
|
29
|
+
# Also accepts :unsubscribe (true or String URL).
|
30
|
+
# @param block [Proc] Block containing Goodmail DSL calls (text, button, etc.)
|
31
|
+
# @return [Mail::Message] The generated Mail object, ready for delivery.
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# mail = Goodmail.compose(to: 'user@example.com', subject: 'Hello!') do
|
35
|
+
# text "This is the email body."
|
36
|
+
# button "Click Me", "https://example.com"
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# mail.deliver_now
|
40
|
+
# # or
|
41
|
+
# mail.deliver_later
|
42
|
+
#
|
43
|
+
def self.compose(headers = {}, &block)
|
44
|
+
# Delegate the actual building process to the Dispatcher
|
45
|
+
Dispatcher.build_message(headers, &block)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Error class is defined in lib/goodmail/error.rb
|
49
|
+
end
|
data/sig/goodmail.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: goodmail
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- rameerez
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-05-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '7.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '7.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rails-html-sanitizer
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: premailer-rails
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.10'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.10'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: pry
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rake
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '13.0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '13.0'
|
82
|
+
description: Send beautiful, simple transactional emails with zero HTML hell. Goodmail
|
83
|
+
is a minimal, opinionated, expressive Ruby DSL for sending production-grade transactional
|
84
|
+
emails in Rails apps that look good in any email client out of the box.
|
85
|
+
email:
|
86
|
+
- rubygems@rameerez.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- CHANGELOG.md
|
92
|
+
- LICENSE.txt
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- lib/goodmail.rb
|
96
|
+
- lib/goodmail/builder.rb
|
97
|
+
- lib/goodmail/configuration.rb
|
98
|
+
- lib/goodmail/dispatcher.rb
|
99
|
+
- lib/goodmail/error.rb
|
100
|
+
- lib/goodmail/layout.erb
|
101
|
+
- lib/goodmail/layout.rb
|
102
|
+
- lib/goodmail/mailer.rb
|
103
|
+
- lib/goodmail/version.rb
|
104
|
+
- sig/goodmail.rbs
|
105
|
+
homepage: https://github.com/rameerez/goodmail
|
106
|
+
licenses:
|
107
|
+
- MIT
|
108
|
+
metadata:
|
109
|
+
allowed_push_host: https://rubygems.org
|
110
|
+
homepage_uri: https://github.com/rameerez/goodmail
|
111
|
+
source_code_uri: https://github.com/rameerez/goodmail
|
112
|
+
changelog_uri: https://github.com/rameerez/goodmail/blob/main/CHANGELOG.md
|
113
|
+
rubygems_mfa_required: 'true'
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: 3.1.0
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubygems_version: 3.6.2
|
129
|
+
specification_version: 4
|
130
|
+
summary: Make your transactional emails look beautiful
|
131
|
+
test_files: []
|