goodmail 0.2.0 → 0.3.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/README.md +68 -3
- data/lib/goodmail/builder.rb +4 -1
- data/lib/goodmail/email.rb +71 -0
- data/lib/goodmail/layout.erb +2 -2
- data/lib/goodmail/version.rb +1 -1
- data/lib/goodmail.rb +1 -0
- data/mailgood.webp +0 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97d19566a8e8eacb10fed28cddfbe7c9f254ae6f4df4b0c2a8ad5259d646631d
|
4
|
+
data.tar.gz: a15578a0b463e3372ffeff6f9d0a2b0aaa3754c0cf604cc574a97fb0506545fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ec18e8bc6a78545259c5a359b1811fa7df18cccdca0df36761b94db25304ea9f894ae85cbddbeb54c674526ad7eb5c5a80e24767f7bd97d23baa692ab02aa5f
|
7
|
+
data.tar.gz: d384ec22ed4107cb20c73deff523e15b3d5dcaf7c3d5a464030024336fde15e6346f5f02f90673e23d6790e085db70782b2a2e0df783dbd7db8ad23908b8e3ff
|
data/README.md
CHANGED
@@ -5,6 +5,10 @@ Send beautiful, simple transactional emails with zero HTML hell.
|
|
5
5
|
|
6
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
7
|
|
8
|
+

|
9
|
+
|
10
|
+
You can easily add buttons, images, links, price lines, and text to your emails, and it'll look good everywhere, no styling needed.
|
11
|
+
|
8
12
|
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
13
|
|
10
14
|
(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!)
|
@@ -45,7 +49,6 @@ Goodmail.configure do |config|
|
|
45
49
|
config.brand_color = "#E62F17"
|
46
50
|
|
47
51
|
# Optional: URL to your company logo. If set, it will appear in the header.
|
48
|
-
# Recommended size: max-height 30px.
|
49
52
|
# Default: nil
|
50
53
|
config.logo_url = "https://cdn.myapp.com/images/email_logo.png"
|
51
54
|
|
@@ -101,7 +104,7 @@ recipient = User.find(params[:user_id])
|
|
101
104
|
|
102
105
|
mail = Goodmail.compose(
|
103
106
|
to: recipient.email,
|
104
|
-
from: "
|
107
|
+
from: "'#{Goodmail.config.company_name} Support' <support@myapp.com>",
|
105
108
|
subject: "Welcome to MyApp!",
|
106
109
|
preheader: "Your adventure begins now!" # Optional override
|
107
110
|
) do
|
@@ -169,6 +172,68 @@ Inside the `Goodmail.compose` block, you have access to these methods:
|
|
169
172
|
* `sign(name = Goodmail.config.company_name)`: Adds a standard closing signature line.
|
170
173
|
* `html(raw_html_string)`: **Use with extreme caution.** Allows embedding raw, *un-sanitized* HTML.
|
171
174
|
|
175
|
+
### Advanced: Rendering Email Parts with `Goodmail.render`
|
176
|
+
|
177
|
+
For more advanced use cases, such as integrating Goodmail's content generation into existing mailer workflows (like Devise mailers) or when you need direct access to the generated HTML and plain text parts before sending, Goodmail provides the `Goodmail.render` method.
|
178
|
+
|
179
|
+
This method processes your DSL block, applies the layout, runs Premailer for CSS inlining, and performs plain text cleanup, similar to `Goodmail.compose`. However, instead of returning a `Mail::Message` object ready for delivery, it returns a `Goodmail::EmailParts` struct.
|
180
|
+
|
181
|
+
The `Goodmail::EmailParts` struct (defined in `goodmail/email.rb`) has two attributes:
|
182
|
+
* `html`: The final, inlined HTML content for your email.
|
183
|
+
* `text`: The cleaned-up plain text version of your email.
|
184
|
+
|
185
|
+
**How to use it:**
|
186
|
+
|
187
|
+
You can then use these parts within any Action Mailer setup:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
# In your custom mailer (e.g., a Devise mailer override)
|
191
|
+
|
192
|
+
# Define your headers (to, from, subject, etc.)
|
193
|
+
# The :subject is crucial for Goodmail.render.
|
194
|
+
# You can also pass :unsubscribe_url and :preheader to Goodmail.render
|
195
|
+
# to override global configurations for that specific email.
|
196
|
+
# Note: these Goodmail-specific keys will be used by Goodmail.render
|
197
|
+
# and should not be passed directly to ActionMailer's mail() method
|
198
|
+
# if they are not standard mail headers.
|
199
|
+
mail_rendering_headers = {
|
200
|
+
to: recipient.email,
|
201
|
+
from: "notifications@myapp.com",
|
202
|
+
subject: "Important Update for #{recipient.name}",
|
203
|
+
unsubscribe_url: custom_unsubscribe_url_for_user(recipient), # Optional
|
204
|
+
preheader: "A quick update you should see." # Optional
|
205
|
+
}
|
206
|
+
|
207
|
+
# Render the email parts using Goodmail's DSL
|
208
|
+
# Goodmail.render will use :subject, :unsubscribe_url, :preheader internally.
|
209
|
+
parts = Goodmail.render(mail_rendering_headers) do
|
210
|
+
h1 "Hello, #{recipient.name}!"
|
211
|
+
text "This is an important update regarding your account."
|
212
|
+
button "View Details", view_details_url(recipient)
|
213
|
+
sign "The MyApp Team"
|
214
|
+
end
|
215
|
+
|
216
|
+
# Prepare headers for ActionMailer's mail() method,
|
217
|
+
# ensuring only standard mail headers are passed.
|
218
|
+
action_mailer_headers = mail_rendering_headers.slice(:to, :from, :subject, :cc, :bcc, :reply_to)
|
219
|
+
|
220
|
+
# Now use these parts in ActionMailer's mail method
|
221
|
+
# You might also want to add the List-Unsubscribe header manually here if needed.
|
222
|
+
final_mail_object = mail(action_mailer_headers) do |format|
|
223
|
+
format.html { render html: parts.html.html_safe }
|
224
|
+
format.text { render plain: parts.text }
|
225
|
+
end
|
226
|
+
|
227
|
+
# The `final_mail_object` returned by ActionMailer can then be delivered:
|
228
|
+
# final_mail_object.deliver_now or final_mail_object.deliver_later
|
229
|
+
```
|
230
|
+
|
231
|
+
**Key Differences from `Goodmail.compose`:**
|
232
|
+
|
233
|
+
* **Return Value**: `Goodmail.render` returns an instance of `Goodmail::EmailParts` (e.g., `EmailParts.new(html: "...", text: "...")`). `Goodmail.compose` returns a `Mail::Message` object.
|
234
|
+
* **Purpose**: `Goodmail.render` is primarily for generating and retrieving processed email content parts. `Goodmail.compose` is for generating a complete, deliverable `Mail::Message` object.
|
235
|
+
* **List-Unsubscribe Header**: `Goodmail.render` itself does *not* add the `List-Unsubscribe` header to any mail object (as it doesn't create one). If you use `Goodmail.render`, you are responsible for adding this header to your `Mail::Message` object if an `unsubscribe_url` was effectively used during rendering (either passed to `Goodmail.render` or taken from global config) and you require this header. The internal `Goodmail::Mailer` (used by `Goodmail.compose`) handles adding this header automatically to the `Mail::Message` object it builds.
|
236
|
+
|
172
237
|
### Adding Unsubscribe Functionality
|
173
238
|
|
174
239
|
Goodmail helps you add the `List-Unsubscribe` header and an optional visible link, but **you must provide the actual URL** where users can unsubscribe.
|
@@ -213,4 +278,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/rameer
|
|
213
278
|
|
214
279
|
## License
|
215
280
|
|
216
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
281
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/goodmail/builder.rb
CHANGED
@@ -6,6 +6,9 @@ module Goodmail
|
|
6
6
|
# Builds the HTML content string based on DSL method calls.
|
7
7
|
class Builder
|
8
8
|
include ERB::Util # For the h() helper
|
9
|
+
# The h helper, included from ERB::Util, stands for html_escape.
|
10
|
+
# It converts special characters (&, <, >, ", ') into their HTML entity equivalents (&, <, >, ", '). This prevents Cross-Site Scripting (XSS) by ensuring dynamic content is displayed as literal text rather than being interpreted as HTML.
|
11
|
+
|
9
12
|
|
10
13
|
# Initialize a basic sanitizer allowing only <a> tags with href
|
11
14
|
HTML_SANITIZER = Rails::Html::SafeListSanitizer.new
|
@@ -83,7 +86,7 @@ module Goodmail
|
|
83
86
|
# Adds a simple price row as a styled paragraph.
|
84
87
|
# NOTE: This does not create a table structure.
|
85
88
|
def price_row(name, price)
|
86
|
-
parts << %(<p style="font-weight:bold; text-align:center; border-top:1px solid #eaeaea; padding:
|
89
|
+
parts << %(<p style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 14px; font-weight:bold; text-align:center; border-top:1px solid #eaeaea; padding:14px 0; margin: 0;">#{h name} – #{h price}</p>)
|
87
90
|
end
|
88
91
|
|
89
92
|
# Adds a simple code box with background styling.
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "premailer"
|
3
|
+
require "cgi" # For unescaping HTML in plaintext generation (though Premailer might handle most)
|
4
|
+
|
5
|
+
module Goodmail
|
6
|
+
# Simple struct to hold the rendered HTML and text parts of an email.
|
7
|
+
EmailParts = Struct.new(:html, :text, keyword_init: true)
|
8
|
+
|
9
|
+
# Renders the email content using the Goodmail DSL and returns HTML and text parts.
|
10
|
+
# This method does not send the email but prepares its content for sending.
|
11
|
+
#
|
12
|
+
# @param headers [Hash] Mail headers. Expected to contain :subject.
|
13
|
+
# Can also contain :unsubscribe_url and :preheader to override defaults.
|
14
|
+
# @param dsl_block [Proc] Block containing Goodmail DSL calls (text, button, etc.)
|
15
|
+
# @return [Goodmail::EmailParts] An object containing the :html and :text email parts.
|
16
|
+
def self.render(headers = {}, &dsl_block)
|
17
|
+
# 1. Initialize the Builder and execute the DSL block
|
18
|
+
builder = Goodmail::Builder.new
|
19
|
+
builder.instance_eval(&dsl_block) if block_given?
|
20
|
+
core_html_content = builder.html_output
|
21
|
+
|
22
|
+
# 2. Determine unsubscribe_url and preheader
|
23
|
+
# These are removed from headers as they are Goodmail-specific, not standard mail headers.
|
24
|
+
current_headers = headers.dup # Avoid modifying the original headers hash directly
|
25
|
+
unsubscribe_url = current_headers.delete(:unsubscribe_url) || Goodmail.config.unsubscribe_url
|
26
|
+
preheader = current_headers.delete(:preheader) || Goodmail.config.default_preheader || current_headers[:subject]
|
27
|
+
|
28
|
+
# 3. Render the raw HTML body using the Layout
|
29
|
+
# The subject is passed for the <title> tag and potentially other uses in layout.
|
30
|
+
# Unsubscribe URL and preheader are passed for inclusion in the layout.
|
31
|
+
raw_html_body = Goodmail::Layout.render(
|
32
|
+
core_html_content,
|
33
|
+
current_headers[:subject], # Use subject from (potentially modified) current_headers
|
34
|
+
unsubscribe_url: unsubscribe_url,
|
35
|
+
preheader: preheader
|
36
|
+
)
|
37
|
+
|
38
|
+
# 4. Use Premailer to inline CSS and generate plaintext
|
39
|
+
premailer = Premailer.new(
|
40
|
+
raw_html_body,
|
41
|
+
with_html_string: true,
|
42
|
+
adapter: :nokogiri,
|
43
|
+
preserve_styles: false, # Force inlining and remove <style> block
|
44
|
+
remove_ids: true, # Remove IDs
|
45
|
+
remove_comments: false # Keep MSO conditional comments
|
46
|
+
)
|
47
|
+
|
48
|
+
final_inlined_html = premailer.to_inline_css
|
49
|
+
generated_plain_text = premailer.to_plain_text
|
50
|
+
|
51
|
+
# 5. Perform refined plaintext cleanup (ported from Goodmail::Mailer)
|
52
|
+
# 5.1. Remove logo alt text line (if logo exists and has associated URL)
|
53
|
+
if Goodmail.config.logo_url.present? && Goodmail.config.company_url.present? && Goodmail.config.company_name.present?
|
54
|
+
company_name_escaped = Regexp.escape(Goodmail.config.company_name)
|
55
|
+
company_url_escaped = Regexp.escape(Goodmail.config.company_url)
|
56
|
+
# Regex to match the typical alt text pattern for a linked logo image
|
57
|
+
logo_alt_pattern = /^\s*#{company_name_escaped}\s+Logo\s*\(.*?#{company_url_escaped}.*?\).*\n?/i
|
58
|
+
generated_plain_text.gsub!(logo_alt_pattern, "")
|
59
|
+
end
|
60
|
+
|
61
|
+
# 5.2. Remove any remaining standalone URL lines (often from logo links or similar artifacts)
|
62
|
+
# This targets lines that consist *only* of a URL.
|
63
|
+
generated_plain_text.gsub!(/^\s*https?:\/\/\S+\s*$\n?/i, "")
|
64
|
+
|
65
|
+
# 5.3. Compact excess blank lines (more than two consecutive newlines)
|
66
|
+
generated_plain_text.gsub!(/\n{3,}/, "\n\n")
|
67
|
+
|
68
|
+
# 6. Return the structured parts
|
69
|
+
EmailParts.new(html: final_inlined_html, text: generated_plain_text.strip)
|
70
|
+
end
|
71
|
+
end
|
data/lib/goodmail/layout.erb
CHANGED
@@ -179,10 +179,10 @@
|
|
179
179
|
<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">
|
180
180
|
<% if config.company_url.present? %>
|
181
181
|
<a href="<%= config.company_url %>" style="text-decoration:none; border:0;">
|
182
|
-
<img src="<%= config.logo_url %>" alt="<%= config.company_name %> Logo" height="
|
182
|
+
<img src="<%= config.logo_url %>" alt="<%= config.company_name %> Logo" height="20" style="max-height: 20px; width: auto; border:0; outline:none; text-decoration:none; display:block;" />
|
183
183
|
</a>
|
184
184
|
<% else %>
|
185
|
-
<img src="<%= config.logo_url %>" alt="<%= config.company_name %> Logo" height="
|
185
|
+
<img src="<%= config.logo_url %>" alt="<%= config.company_name %> Logo" height="20" style="max-height: 20px; width: auto; border:0; outline:none; text-decoration:none; display:block;" />
|
186
186
|
<% end %>
|
187
187
|
</td>
|
188
188
|
</tr>
|
data/lib/goodmail/version.rb
CHANGED
data/lib/goodmail.rb
CHANGED
@@ -10,6 +10,7 @@ require_relative "goodmail/configuration"
|
|
10
10
|
require_relative "goodmail/error" # Load Error class explicitly if needed elsewhere
|
11
11
|
require_relative "goodmail/builder"
|
12
12
|
require_relative "goodmail/layout"
|
13
|
+
require_relative "goodmail/email"
|
13
14
|
require_relative "goodmail/mailer" # Require the internal Mailer
|
14
15
|
require_relative "goodmail/dispatcher"
|
15
16
|
|
data/mailgood.webp
ADDED
Binary file
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: goodmail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- rameerez
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-05-
|
10
|
+
date: 2025-05-14 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -96,11 +96,13 @@ files:
|
|
96
96
|
- lib/goodmail/builder.rb
|
97
97
|
- lib/goodmail/configuration.rb
|
98
98
|
- lib/goodmail/dispatcher.rb
|
99
|
+
- lib/goodmail/email.rb
|
99
100
|
- lib/goodmail/error.rb
|
100
101
|
- lib/goodmail/layout.erb
|
101
102
|
- lib/goodmail/layout.rb
|
102
103
|
- lib/goodmail/mailer.rb
|
103
104
|
- lib/goodmail/version.rb
|
105
|
+
- mailgood.webp
|
104
106
|
- sig/goodmail.rbs
|
105
107
|
homepage: https://github.com/rameerez/goodmail
|
106
108
|
licenses:
|