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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46415351f6fd50d3944eb427590536c352575a1c49b417920a09664936a5743d
4
- data.tar.gz: 23853af7b4d45b9f099cb20538e5a535f2d373e78320337f8400238ca1d99786
3
+ metadata.gz: 97d19566a8e8eacb10fed28cddfbe7c9f254ae6f4df4b0c2a8ad5259d646631d
4
+ data.tar.gz: a15578a0b463e3372ffeff6f9d0a2b0aaa3754c0cf604cc574a97fb0506545fe
5
5
  SHA512:
6
- metadata.gz: 884e71765428aa8cd6d4529c8c451adde02c4b8d8e8053b4bc5ec23aed2612fcf36160afa1b0499795e87f0c6c5952d6dee1ea1510dff0e0a0cdf95531965b2a
7
- data.tar.gz: edc98703ff32581d788ff151ded6961f5589446b23a2fe7aba25dfe67bf579ee2d56d1b35b58aa2901ec06f89392eb880b44332a1b8bca594b0a8ef5a9997d3e
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
+ ![Goodmail Example Email](mailgood.webp)
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: ""#{Goodmail.config.company_name} Support" <support@myapp.com>",
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).
@@ -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 (&amp;, &lt;, &gt;, &quot;, &#39;). 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:20px 0; margin: 0;">#{h name} &ndash; #{h price}</p>)
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} &nbsp; &ndash; &nbsp; #{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
@@ -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="30" style="max-height: 30px; width: auto; border:0; outline:none; text-decoration:none; display:block;" />
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="30" style="max-height: 30px; width: auto; border:0; outline:none; text-decoration:none; display:block;" />
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>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Goodmail
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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.2.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-02 00:00:00.000000000 Z
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: