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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa74c986faf7a129fa8daa4546192918312488290ad46aff8f889009efdefbe4
4
- data.tar.gz: 9d138231f61b68a137a2726b989788bd180d7c15e6fa6a4f414c929243c8b343
3
+ metadata.gz: 46415351f6fd50d3944eb427590536c352575a1c49b417920a09664936a5743d
4
+ data.tar.gz: 23853af7b4d45b9f099cb20538e5a535f2d373e78320337f8400238ca1d99786
5
5
  SHA512:
6
- metadata.gz: 66d62b19ff6baebd3512bc3d04f60b0b329fec2e36b90ccd8cc9701a955206b91f73f86af4ccdb9a74501f0e05d4f36c948d1683b54049fdebf821244965331c
7
- data.tar.gz: a1c74368751fc8c5701f6aefc588a118ac1839691d5657475024343e0b007646271e9ffe1b7c23294245dfaedde263e421a4b24418a3df0fc74998a983f49ab3
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
- [![Gem Version](https://badge.fury.io/rb/pay.svg)](https://badge.fury.io/rb/pay)
1
+ # 💌 Goodmail - Make your Rails SaaS transactional emails look beautiful
2
+ [![Gem Version](https://badge.fury.io/rb/goodmail.svg)](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
- You can (and should!) edit the default strings in the Goodmail initializer.
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
- # 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
36
+ # --- Basic Branding (Required) ---
39
37
 
40
- # The company name displayed in the email footer and used by the default `sign` helper.
41
- # Default: "Example Inc."
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: URL to your company logo. If set, it will appear centered in the header.
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: Custom text displayed in the footer below the copyright.
50
- # Use this to explain why the user received the email.
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.footer_text = "You are receiving this email because you signed up for an account at MyApp."
55
+ config.company_url = "https://myapp.com"
53
56
 
54
- # Optional: Global default URL for unsubscribe links (both the List-Unsubscribe
55
- # header and the optional visible link in the footer).
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 (your usual standard Action Mailer methods)
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
- # 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
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. 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.
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. Only use this if you absolutely trust the source of the string.
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. Goodmail does not generate unsubscribe URLs or handle the logic.
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:** 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.
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
- unsubscribe_url: manage_subscription_url(recipient) # Your app's URL helper
133
- ) do
134
- # ... email content ...
135
- end
185
+ ) do # ...
136
186
  ```
137
- *If an `unsubscribe_url` is provided (either globally or per-email), Goodmail will automatically add the standard `List-Unsubscribe` header.*
187
+ *If an `unsubscribe_url` is provided, Goodmail adds the `List-Unsubscribe` header.*
138
188
 
139
189
  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.*
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
 
@@ -34,16 +34,62 @@ module Goodmail
34
34
  end
35
35
 
36
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>)
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
- style = "max-width:100%; height:auto;"
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
- # Use a class for easier styling via layout CSS
46
- parts << %(<img class="goodmail-image" src="#{h src}" alt="#{h alt}" style="#{style}">)
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} &ndash; #{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
- # 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>)
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 footer text (e.g., "Why you received this email")
13
- footer_text: nil,
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
- # Allows users to modify the configuration in an initializer:
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
- # Ensure config is initialized before yielding
31
- yield config
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
- # Removed attr_reader/writer - relying on explicit config method.
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
@@ -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. Render the raw HTML body using the Layout
25
- # This raw HTML still has the <style> block needed by Premailer.
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
- # 5. Slice standard headers for the mailer action
35
+ # 6. Slice standard headers for the mailer action
33
36
  mailer_headers = slice_mail_headers(headers)
34
37
 
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
+ # 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
- # 7. Return the ActionMailer::MessageDelivery object
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
@@ -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
- <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;" />
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">
@@ -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}"
@@ -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, raw_text_body, unsubscribe_url)
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, # 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
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Goodmail
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: goodmail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez