goodmail 0.1.0 → 0.2.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/CHANGELOG.md +16 -0
- data/README.md +91 -41
- data/lib/goodmail/builder.rb +53 -8
- data/lib/goodmail/configuration.rb +28 -12
- data/lib/goodmail/dispatcher.rb +10 -8
- data/lib/goodmail/layout.erb +14 -2
- data/lib/goodmail/layout.rb +4 -2
- data/lib/goodmail/mailer.rb +22 -6
- data/lib/goodmail/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46415351f6fd50d3944eb427590536c352575a1c49b417920a09664936a5743d
|
4
|
+
data.tar.gz: 23853af7b4d45b9f099cb20538e5a535f2d373e78320337f8400238ca1d99786
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 884e71765428aa8cd6d4529c8c451adde02c4b8d8e8053b4bc5ec23aed2612fcf36160afa1b0499795e87f0c6c5952d6dee1ea1510dff0e0a0cdf95531965b2a
|
7
|
+
data.tar.gz: edc98703ff32581d788ff151ded6961f5589446b23a2fe7aba25dfe67bf579ee2d56d1b35b58aa2901ec06f89392eb880b44332a1b8bca594b0a8ef5a9997d3e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
## [0.2.0] - 2025-05-02
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- **New DSL Methods:** Added `code_box` and `price_row` for displaying formatted content like license keys or simple line items.
|
6
|
+
- **Configuration Validation:** Added validation to ensure required config keys (`company_name`, `brand_color`) are set on application startup.
|
7
|
+
- **Clickable Logo:** Added `config.company_url` to make the header logo clickable.
|
8
|
+
- **Preheader Text:** Added support for hidden preheader text via `config.default_preheader` and `headers[:preheader]`.
|
9
|
+
- **Schema.org Microdata:** Included basic Schema.org (`EmailMessage`, `ConfirmAction`) markup in the layout template for potential enhancements in email clients (like Gmail action buttons).
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
|
13
|
+
- Resolved Action Mailer errors related to template lookups and `deliver_later` safety checks.
|
14
|
+
- Correctly added `.html_safe` to MSO conditional comment wrappers in Builder to prevent Rails from escaping them.
|
15
|
+
- Corrected plaintext generation issues related to image alt text and signatures.
|
16
|
+
|
1
17
|
## [0.1.0] - 2025-05-02
|
2
18
|
|
3
19
|
- Initial release
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
# 💌 Goodmail - Make your transactional emails look beautiful
|
2
|
-
[](https://badge.fury.io/rb/goodmail)
|
3
3
|
|
4
4
|
Send beautiful, simple transactional emails with zero HTML hell.
|
5
5
|
|
@@ -25,39 +25,55 @@ bundle install
|
|
25
25
|
|
26
26
|
## Configuration
|
27
27
|
|
28
|
-
|
28
|
+
Goodmail requires minimal configuration to ensure emails look correct. You **must** set at least your `company_name`.
|
29
29
|
|
30
|
-
Create an initializer file at `config/initializers/goodmail.rb
|
30
|
+
Create an initializer file at `config/initializers/goodmail.rb` and configure the options:
|
31
31
|
|
32
32
|
```ruby
|
33
33
|
# config/initializers/goodmail.rb
|
34
34
|
|
35
35
|
Goodmail.configure do |config|
|
36
|
-
#
|
37
|
-
# Default: "#348eda"
|
38
|
-
config.brand_color = "#E62F17" # Your brand's primary color
|
36
|
+
# --- Basic Branding (Required) ---
|
39
37
|
|
40
|
-
# The company name displayed in the email footer and used by
|
41
|
-
#
|
38
|
+
# The company name displayed in the email footer and used by `sign` helper.
|
39
|
+
# NOT OPTIONAL - MUST BE SET
|
42
40
|
config.company_name = "MyApp Inc."
|
43
41
|
|
44
|
-
# Optional
|
42
|
+
# --- Optional Branding ---
|
43
|
+
|
44
|
+
# The main accent color used for buttons and links in the email body.
|
45
|
+
config.brand_color = "#E62F17"
|
46
|
+
|
47
|
+
# Optional: URL to your company logo. If set, it will appear in the header.
|
45
48
|
# Recommended size: max-height 30px.
|
46
49
|
# Default: nil
|
47
50
|
config.logo_url = "https://cdn.myapp.com/images/email_logo.png"
|
48
51
|
|
49
|
-
# Optional:
|
50
|
-
#
|
52
|
+
# Optional: URL the header logo links to (e.g., your homepage).
|
53
|
+
# Ignored if logo_url is not set. Must be a valid URL (no spaces etc.).
|
51
54
|
# Default: nil
|
52
|
-
config.
|
55
|
+
config.company_url = "https://myapp.com"
|
53
56
|
|
54
|
-
# Optional
|
55
|
-
|
57
|
+
# --- Optional Email Content Defaults ---
|
58
|
+
|
59
|
+
# Optional: Default preheader text (appears after subject in inbox preview).
|
60
|
+
# Can be overridden per email via headers[:preheader]. If unset, subject is used.
|
61
|
+
# Default: nil
|
62
|
+
config.default_preheader = "Your account update from MyApp."
|
63
|
+
|
64
|
+
# Optional: Global default URL for unsubscribe links.
|
56
65
|
# Goodmail *does not* handle the unsubscribe logic; you must provide a valid URL.
|
57
66
|
# Can be overridden per email via headers[:unsubscribe_url].
|
58
67
|
# Default: nil
|
59
68
|
config.unsubscribe_url = "https://myapp.com/emails/unsubscribe"
|
60
69
|
|
70
|
+
# --- Optional Footer Customization ---
|
71
|
+
|
72
|
+
# Optional: Custom text displayed in the footer below the copyright.
|
73
|
+
# Use this to explain why the user received the email.
|
74
|
+
# Default: nil
|
75
|
+
config.footer_text = "You are receiving this email because you signed up for an account at MyApp."
|
76
|
+
|
61
77
|
# Optional: Whether to show a visible unsubscribe link in the footer.
|
62
78
|
# Requires an unsubscribe URL to be set (globally or per-email).
|
63
79
|
# Default: false
|
@@ -69,21 +85,34 @@ Goodmail.configure do |config|
|
|
69
85
|
end
|
70
86
|
```
|
71
87
|
|
88
|
+
*The application will raise an error on startup if required configuration keys (`company_name`) are missing or blank.*
|
89
|
+
|
72
90
|
Make sure to restart your Rails server after creating or modifying the initializer.
|
73
91
|
|
74
92
|
## Quick start
|
75
93
|
|
76
|
-
Use the `Goodmail.compose` method to compose emails using the DSL, then call `.deliver_now` or `.deliver_later` on it
|
94
|
+
Use the `Goodmail.compose` method to compose emails using the DSL, then call `.deliver_now` or `.deliver_later` on it.
|
77
95
|
|
78
|
-
### Basic Example
|
96
|
+
### Basic Example (Deliver Now)
|
79
97
|
|
80
98
|
```ruby
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
99
|
+
# Assumes config/initializers/goodmail.rb is configured!
|
100
|
+
recipient = User.find(params[:user_id])
|
101
|
+
|
102
|
+
mail = Goodmail.compose(
|
103
|
+
to: recipient.email,
|
104
|
+
from: ""#{Goodmail.config.company_name} Support" <support@myapp.com>",
|
105
|
+
subject: "Welcome to MyApp!",
|
106
|
+
preheader: "Your adventure begins now!" # Optional override
|
107
|
+
) do
|
108
|
+
h1 "Welcome aboard, #{recipient.name}!"
|
109
|
+
text "We're thrilled to have you join the MyApp community."
|
110
|
+
text "Here are a few things: Check the <a href=\"/help\">Help Center</a>."
|
111
|
+
button "Go to Dashboard", user_dashboard_url(recipient)
|
112
|
+
sign
|
113
|
+
end
|
114
|
+
|
115
|
+
mail.deliver_now
|
87
116
|
```
|
88
117
|
|
89
118
|
### Deliver Later (Background Job)
|
@@ -102,52 +131,73 @@ mail.deliver_later
|
|
102
131
|
|
103
132
|
*(Requires Active Job configured.)*
|
104
133
|
|
134
|
+
## Why does `goodmail` exist?
|
135
|
+
|
136
|
+
Here's the problem: you can't just use standard HTML and CSS in mails.
|
137
|
+
|
138
|
+
Emails are notoriously complicated to work with, because they're very difficult to style.
|
139
|
+
|
140
|
+
Modern CSS doesn't work in mails, because email clients render styles differently and some only support a primitive subset of HTML / CSS.
|
141
|
+
|
142
|
+
So, for example, you can't use stylesheets **at all**: all CSS needs to be inlined. You can't use many modern CSS properties either.
|
143
|
+
|
144
|
+
This is why many emails still use `<table>` elements, for example. It's the only way of making mails look good!
|
145
|
+
|
146
|
+
In fact, Mailgun released years ago [a few battle-tested HTML templates for emails](https://github.com/mailgun/transactional-email-templates). I took one of those email templates and have been using it in my projects for years.
|
147
|
+
|
148
|
+
So, can't this just be an Action Mailer `.erb` template instead?
|
149
|
+
|
150
|
+
I thought the same! And that's actually how I started using it. But after using it for years I realized I ended up building my own "proto-DSL" around it: I decomposed the email HTML template in partials, I was copying the same partials from project to project, etc. And setting up good emails in every new project took me a while because each project would have slight inconsistencies in the mail partials.
|
151
|
+
|
152
|
+
So making it into a gem with a simple DSL was my solution to solve this email HTML mess.
|
153
|
+
|
154
|
+
## Usage
|
155
|
+
|
105
156
|
### Available DSL Methods
|
106
157
|
|
107
158
|
Inside the `Goodmail.compose` block, you have access to these methods:
|
108
159
|
|
109
160
|
* `h1(text)`, `h2(text)`, `h3(text)`: Styled heading tags.
|
110
|
-
* `text(string)`: A paragraph of text.
|
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.
|
161
|
+
* `text(string)`: A paragraph of text. Allows simple inline `<a>` tags with `href` attributes; other HTML is stripped for safety. Handles `\n` for line breaks.
|
162
|
+
* `button(link_text, url)`: A prominent, styled call-to-action button (includes Outlook VML fallback).
|
163
|
+
* `image(src, alt = "", width: nil, height: nil)`: Embeds an image, centered by default (includes Outlook MSO fallback). Uses `config.company_name` for alt text if none provided.
|
113
164
|
* `space(pixels = 16)`: Adds vertical whitespace.
|
114
165
|
* `line`: Adds a horizontal rule (`<hr>`).
|
115
166
|
* `center { ... }`: Centers the content generated within the block.
|
167
|
+
* `code_box(text)`: Displays text centered and bold within a styled box (grey background, padding, italic). Text is HTML-escaped.
|
168
|
+
* `price_row(name, price)`: Adds a styled paragraph showing a name and price, separated by a top border (e.g., for simple receipt line items). Text is HTML-escaped.
|
116
169
|
* `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.
|
170
|
+
* `html(raw_html_string)`: **Use with extreme caution.** Allows embedding raw, *un-sanitized* HTML.
|
118
171
|
|
119
172
|
### Adding Unsubscribe Functionality
|
120
173
|
|
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.
|
174
|
+
Goodmail helps you add the `List-Unsubscribe` header and an optional visible link, but **you must provide the actual URL** where users can unsubscribe.
|
122
175
|
|
123
|
-
1. **Provide the URL:**
|
124
|
-
* **Globally:** Set `config.unsubscribe_url = "your_global_url"
|
125
|
-
* **Per-Email:** Pass `unsubscribe_url: "your_specific_url"` in the headers hash
|
176
|
+
1. **Provide the URL:**
|
177
|
+
* **Globally:** Set `config.unsubscribe_url = "your_global_url"`.
|
178
|
+
* **Per-Email:** Pass `unsubscribe_url: "your_specific_url"` in the headers hash. This overrides the global setting.
|
126
179
|
|
127
180
|
```ruby
|
128
|
-
# Example using per-email override
|
129
181
|
mail = Goodmail.compose(
|
130
182
|
to: recipient.email,
|
183
|
+
unsubscribe_url: manage_subscription_url(recipient),
|
131
184
|
# ... other headers ...
|
132
|
-
|
133
|
-
) do
|
134
|
-
# ... email content ...
|
135
|
-
end
|
185
|
+
) do # ...
|
136
186
|
```
|
137
|
-
*If an `unsubscribe_url` is provided
|
187
|
+
*If an `unsubscribe_url` is provided, Goodmail adds the `List-Unsubscribe` header.*
|
138
188
|
|
139
189
|
2. **Optionally Show Footer Link:**
|
140
|
-
*
|
141
|
-
*
|
142
|
-
* *
|
190
|
+
* Set `config.show_footer_unsubscribe_link = true`.
|
191
|
+
* Customize `config.footer_unsubscribe_link_text`.
|
192
|
+
* *The footer link only appears if an `unsubscribe_url` was provided AND `config.show_footer_unsubscribe_link` is true.*
|
143
193
|
|
144
194
|
```ruby
|
145
195
|
# config/initializers/goodmail.rb
|
146
196
|
Goodmail.configure do |config|
|
147
|
-
# ... other settings
|
148
197
|
config.unsubscribe_url = "https://myapp.com/preferences"
|
149
198
|
config.show_footer_unsubscribe_link = true
|
150
199
|
config.footer_unsubscribe_link_text = "Manage email preferences"
|
200
|
+
# ...
|
151
201
|
end
|
152
202
|
```
|
153
203
|
|
data/lib/goodmail/builder.rb
CHANGED
@@ -34,16 +34,62 @@ module Goodmail
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def button(text, url)
|
37
|
-
#
|
38
|
-
|
37
|
+
# Standard HTML button link
|
38
|
+
button_html = %(<a href="#{h url}" class="goodmail-button-link" style="color:#ffffff;">#{h text}</a>)
|
39
|
+
# VML fallback for Outlook
|
40
|
+
vml_button = <<~VML
|
41
|
+
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="#{h url}" style="height:44px; v-text-anchor:middle; width:200px;" arcsize="10%" stroke="f" fillcolor="#{Goodmail.config.brand_color}">
|
42
|
+
<w:anchorlock/>
|
43
|
+
<center style="color:#ffffff; font-family:sans-serif; font-size:14px; font-weight:bold;">
|
44
|
+
#{h text}
|
45
|
+
</center>
|
46
|
+
</v:roundrect>
|
47
|
+
VML
|
48
|
+
# MSO conditional wrapper
|
49
|
+
mso_wrapper = <<~MSO
|
50
|
+
<!--[if mso]>
|
51
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;"><tr><td style="padding: 10px 0;" align="center">
|
52
|
+
#{vml_button.strip}
|
53
|
+
</td></tr></table>
|
54
|
+
<![endif]-->
|
55
|
+
<!--[if !mso]><!-->
|
56
|
+
#{button_html}
|
57
|
+
<!--<![endif]-->
|
58
|
+
MSO
|
59
|
+
# Final container div with class for primary CSS styling
|
60
|
+
parts << %(<div class="goodmail-button" style="text-align: center; margin: 24px 0;">#{mso_wrapper.strip.html_safe}</div>)
|
39
61
|
end
|
40
62
|
|
41
63
|
def image(src, alt = "", width: nil, height: nil)
|
42
|
-
|
64
|
+
alt_text = alt.present? ? alt : Goodmail.config.company_name # Default alt text
|
65
|
+
style = "max-width:100%; height:auto; display: block; margin: 0 auto;"
|
43
66
|
style += " width:#{width}px;" if width
|
44
67
|
style += " height:#{height}px;" if height
|
45
|
-
#
|
46
|
-
|
68
|
+
# Standard image tag
|
69
|
+
img_tag = %(<img class="goodmail-image" src="#{h src}" alt="#{h alt_text}" style="#{style}">)
|
70
|
+
# MSO conditional wrapper for centering
|
71
|
+
mso_wrapper = <<~MSO
|
72
|
+
<!--[if mso]>
|
73
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing:0; border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;"><tr><td style="padding: 20px 0;" align="center">
|
74
|
+
<![endif]-->
|
75
|
+
#{img_tag}
|
76
|
+
<!--[if mso]>
|
77
|
+
</td></tr></table>
|
78
|
+
<![endif]-->
|
79
|
+
MSO
|
80
|
+
parts << mso_wrapper.strip.html_safe
|
81
|
+
end
|
82
|
+
|
83
|
+
# Adds a simple price row as a styled paragraph.
|
84
|
+
# NOTE: This does not create a table structure.
|
85
|
+
def price_row(name, price)
|
86
|
+
parts << %(<p style="font-weight:bold; text-align:center; border-top:1px solid #eaeaea; padding:20px 0; margin: 0;">#{h name} – #{h price}</p>)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Adds a simple code box with background styling.
|
90
|
+
def code_box(text)
|
91
|
+
# Re-added background/padding; content is simple, should survive Premailer plain text.
|
92
|
+
parts << %(<p style="background:#F8F8F8; padding:20px; font-style:italic; text-align:center; color:#404040; margin:16px 0; border-radius: 4px;"><strong>#{h text}</strong></p>)
|
47
93
|
end
|
48
94
|
|
49
95
|
def space(px = 16)
|
@@ -52,9 +98,8 @@ module Goodmail
|
|
52
98
|
end
|
53
99
|
|
54
100
|
def sign(name = Goodmail.config.company_name)
|
55
|
-
#
|
56
|
-
|
57
|
-
parts << %(<p style="margin:16px 0; line-height: 1.6;"><span style="color: #888;">– #{h name}</span></p>)
|
101
|
+
# Use #777 for better contrast than #888
|
102
|
+
parts << %(<p style="margin:16px 0; line-height: 1.6;"><span style="color: #777;">– #{h name}</span></p>)
|
58
103
|
end
|
59
104
|
|
60
105
|
%i[h1 h2 h3].each do |heading_tag|
|
@@ -9,32 +9,33 @@ module Goodmail
|
|
9
9
|
brand_color: "#348eda",
|
10
10
|
company_name: "Example Inc.",
|
11
11
|
logo_url: nil,
|
12
|
-
# Optional
|
13
|
-
|
12
|
+
# Optional: URL the header logo links to.
|
13
|
+
company_url: nil,
|
14
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
15
|
unsubscribe_url: nil,
|
16
|
+
# Optional: Default preheader text (appears after subject in inbox preview).
|
17
|
+
default_preheader: nil,
|
18
|
+
# Optional footer text (e.g., "Why you received this email")
|
19
|
+
footer_text: nil,
|
18
20
|
# Show a visible unsubscribe link in the footer?
|
19
21
|
show_footer_unsubscribe_link: false,
|
20
22
|
# Text for the footer unsubscribe link
|
21
23
|
footer_unsubscribe_link_text: "Unsubscribe"
|
22
24
|
).freeze # Freeze the default object to prevent accidental modification
|
23
25
|
|
26
|
+
# Define required keys that MUST be set by the user (cannot be nil/empty)
|
27
|
+
REQUIRED_CONFIG_KEYS = %i[company_name].freeze
|
28
|
+
|
24
29
|
# Provides the configuration block helper.
|
25
|
-
#
|
26
|
-
# Goodmail.configure do |config|
|
27
|
-
# config.brand_color = "#ff0000"
|
28
|
-
# end
|
30
|
+
# Ensures validation runs after the block is executed.
|
29
31
|
def configure
|
30
|
-
#
|
31
|
-
|
32
|
+
yield config # Ensures config is initialized via accessor
|
33
|
+
validate_config!(config)
|
32
34
|
end
|
33
35
|
|
34
36
|
# Returns the current configuration object.
|
35
37
|
# Initializes with a copy of the defaults if not already configured.
|
36
38
|
def config
|
37
|
-
# Use defined? check for more robust initialization in edge cases
|
38
39
|
@config = DEFAULT_CONFIG.dup unless defined?(@config) && @config
|
39
40
|
@config
|
40
41
|
end
|
@@ -45,6 +46,21 @@ module Goodmail
|
|
45
46
|
@config = nil
|
46
47
|
end
|
47
48
|
|
48
|
-
|
49
|
+
private
|
50
|
+
|
51
|
+
# Validates that required configuration keys are set.
|
52
|
+
# Raises Goodmail::Error if any required keys are missing or blank.
|
53
|
+
def validate_config!(current_config)
|
54
|
+
missing_keys = REQUIRED_CONFIG_KEYS.select do |key|
|
55
|
+
value = current_config[key]
|
56
|
+
value.nil? || (value.respond_to?(:strip) && value.strip.empty?)
|
57
|
+
end
|
58
|
+
|
59
|
+
unless missing_keys.empty?
|
60
|
+
raise Goodmail::Error, "Missing required Goodmail configuration keys: #{missing_keys.join(', ')}. Please set them in config/initializers/goodmail.rb"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Optional: Add validation for URL formats if needed
|
64
|
+
end
|
49
65
|
end
|
50
66
|
end
|
data/lib/goodmail/dispatcher.rb
CHANGED
@@ -21,20 +21,21 @@ module Goodmail
|
|
21
21
|
# 3. Determine the final unsubscribe URL (user-provided)
|
22
22
|
unsubscribe_url = headers[:unsubscribe_url] || Goodmail.config.unsubscribe_url
|
23
23
|
|
24
|
-
# 4.
|
25
|
-
|
24
|
+
# 4. Determine preheader text (priority: header > config > subject)
|
25
|
+
preheader = headers[:preheader] || Goodmail.config.default_preheader || headers[:subject]
|
26
|
+
|
27
|
+
# 5. Render the raw HTML body using the Layout
|
26
28
|
raw_html_body = Goodmail::Layout.render(
|
27
29
|
builder.html_output,
|
28
30
|
headers[:subject],
|
29
|
-
unsubscribe_url: unsubscribe_url
|
31
|
+
unsubscribe_url: unsubscribe_url,
|
32
|
+
preheader: preheader # Pass preheader to layout
|
30
33
|
)
|
31
34
|
|
32
|
-
#
|
35
|
+
# 6. Slice standard headers for the mailer action
|
33
36
|
mailer_headers = slice_mail_headers(headers)
|
34
37
|
|
35
|
-
#
|
36
|
-
# Pass the raw HTML (for Premailer) and unsubscribe URL.
|
37
|
-
# Plaintext is now generated by Premailer inside the mailer action.
|
38
|
+
# 7. Build the mail object via the internal Mailer class action.
|
38
39
|
delivery_object = Goodmail::Mailer.compose_message(
|
39
40
|
mailer_headers,
|
40
41
|
raw_html_body,
|
@@ -42,13 +43,14 @@ module Goodmail
|
|
42
43
|
unsubscribe_url
|
43
44
|
)
|
44
45
|
|
45
|
-
#
|
46
|
+
# 8. Return the ActionMailer::MessageDelivery object
|
46
47
|
delivery_object
|
47
48
|
end
|
48
49
|
|
49
50
|
private
|
50
51
|
|
51
52
|
# Whitelist standard headers to pass to ActionMailer's mail() method
|
53
|
+
# Excludes custom headers like :unsubscribe_url, :preheader
|
52
54
|
def slice_mail_headers(h)
|
53
55
|
h.slice(:to, :from, :cc, :bcc, :reply_to, :subject)
|
54
56
|
end
|
data/lib/goodmail/layout.erb
CHANGED
@@ -161,6 +161,11 @@
|
|
161
161
|
|
162
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
163
|
|
164
|
+
<!-- Hidden Preheader Text -->
|
165
|
+
<span style="display:none !important; font-size:1px; color:#ffffff; line-height:1px; max-height:0px; max-width:0px; opacity:0; overflow:hidden;">
|
166
|
+
<%= preheader || subject %>
|
167
|
+
</span>
|
168
|
+
|
164
169
|
<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
170
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
166
171
|
<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>
|
@@ -172,16 +177,23 @@
|
|
172
177
|
<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
178
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
174
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">
|
175
|
-
|
180
|
+
<% if config.company_url.present? %>
|
181
|
+
<a href="<%= config.company_url %>" style="text-decoration:none; border:0;">
|
182
|
+
<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;" />
|
183
|
+
</a>
|
184
|
+
<% else %>
|
185
|
+
<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;" />
|
186
|
+
<% end %>
|
176
187
|
</td>
|
177
188
|
</tr>
|
178
189
|
</table>
|
179
190
|
<% end %>
|
180
191
|
|
181
192
|
<!-- 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">
|
193
|
+
<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" 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
194
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
184
195
|
<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">
|
196
|
+
<meta itemprop="name" content="<%= subject %>" />
|
185
197
|
<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
198
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
187
199
|
<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top">
|
data/lib/goodmail/layout.rb
CHANGED
@@ -16,8 +16,9 @@ module Goodmail
|
|
16
16
|
# @param subject [String] The email subject line (used for the <title> tag).
|
17
17
|
# @param layout_path [String, nil] Optional path to a custom layout ERB file.
|
18
18
|
# @param unsubscribe_url [String, nil] Optional URL for the footer unsubscribe link.
|
19
|
+
# @param preheader [String, nil] Optional preheader text.
|
19
20
|
# @return [String] The full HTML document string.
|
20
|
-
def render(body_html, subject, layout_path: nil, unsubscribe_url: nil)
|
21
|
+
def render(body_html, subject, layout_path: nil, unsubscribe_url: nil, preheader: nil)
|
21
22
|
template_path = layout_path || DEFAULT_LAYOUT_PATH
|
22
23
|
|
23
24
|
unless File.exist?(template_path)
|
@@ -31,7 +32,8 @@ module Goodmail
|
|
31
32
|
body_html: body_html,
|
32
33
|
subject: subject || "",
|
33
34
|
config: Goodmail.config, # Make config available
|
34
|
-
unsubscribe_url: unsubscribe_url # Pass unsubscribe URL to template
|
35
|
+
unsubscribe_url: unsubscribe_url, # Pass unsubscribe URL to template
|
36
|
+
preheader: preheader # Pass preheader to template
|
35
37
|
)
|
36
38
|
rescue => e
|
37
39
|
raise Goodmail::Error, "Failed to render layout template: #{e.message}"
|
data/lib/goodmail/mailer.rb
CHANGED
@@ -18,22 +18,38 @@ module Goodmail
|
|
18
18
|
# Action Mailer wraps the result in a MessageDelivery object.
|
19
19
|
# It uses Premailer to inline CSS and generate plaintext.
|
20
20
|
# @api internal
|
21
|
-
def compose_message(headers, raw_html_body,
|
21
|
+
def compose_message(headers, raw_html_body, _raw_text_body, unsubscribe_url)
|
22
22
|
# Initialize Premailer with the raw HTML body from the layout
|
23
23
|
premailer = Premailer.new(
|
24
24
|
raw_html_body,
|
25
25
|
with_html_string: true,
|
26
26
|
# Common options:
|
27
|
-
adapter: :nokogiri,
|
28
|
-
preserve_styles:
|
29
|
-
remove_ids:
|
30
|
-
remove_comments:
|
27
|
+
adapter: :nokogiri,
|
28
|
+
preserve_styles: false, # Set to false to force inlining *and* remove <style> block
|
29
|
+
remove_ids: true, # Can usually remove IDs
|
30
|
+
remove_comments: false, # KEEP conditional comments so MSO conditionals in the template work!
|
31
|
+
# Note: `plain_text_images: false` might exist but is not standard;
|
32
|
+
# relying on gsub cleanup below.
|
31
33
|
)
|
32
34
|
|
33
35
|
# Get processed content
|
34
36
|
inlined_html = premailer.to_inline_css
|
37
|
+
# Generate plain text, skipping image conversion
|
35
38
|
generated_plain_text = premailer.to_plain_text
|
36
39
|
|
40
|
+
# Clean up plaintext:
|
41
|
+
# 1. Remove logo alt text line (if logo exists and has associated URL)
|
42
|
+
if Goodmail.config.logo_url.present? && Goodmail.config.company_url.present? && Goodmail.config.company_name.present?
|
43
|
+
company_name_escaped = Regexp.escape(Goodmail.config.company_name)
|
44
|
+
company_url_escaped = Regexp.escape(Goodmail.config.company_url)
|
45
|
+
logo_alt_pattern = /^\s*#{company_name_escaped}\s+Logo\s+\(.*?#{company_url_escaped}.*?\).*\n?/i
|
46
|
+
generated_plain_text.gsub!(logo_alt_pattern, '')
|
47
|
+
end
|
48
|
+
# 2. Remove any remaining standalone URL lines (often from logo links)
|
49
|
+
generated_plain_text.gsub!(/^\s*https?:\/\/\S+\s*$\n?/i, '')
|
50
|
+
# 3. Compact excess blank lines created by gsubbing
|
51
|
+
generated_plain_text.gsub!(/\n{3,}/, "\n\n")
|
52
|
+
|
37
53
|
# Add List-Unsubscribe header to the headers hash *before* calling mail()
|
38
54
|
if unsubscribe_url.is_a?(String) && !unsubscribe_url.strip.empty?
|
39
55
|
headers["List-Unsubscribe"] = "<#{unsubscribe_url.strip}>"
|
@@ -42,7 +58,7 @@ module Goodmail
|
|
42
58
|
# Call the instance-level `mail` method
|
43
59
|
mail(headers) do |format|
|
44
60
|
# Use the premailer-generated plaintext
|
45
|
-
format.text { render plain: generated_plain_text }
|
61
|
+
format.text { render plain: generated_plain_text.strip }
|
46
62
|
# Use the CSS-inlined HTML
|
47
63
|
format.html { render html: inlined_html.html_safe }
|
48
64
|
end
|
data/lib/goodmail/version.rb
CHANGED