goodmail 0.3.1 → 0.4.1
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/.rubocop.yml +199 -0
- data/.simplecov +55 -0
- data/CHANGELOG.md +79 -0
- data/README.md +127 -55
- data/Rakefile +12 -1
- data/context7.json +4 -0
- data/examples/PAY_TESTING_GUIDE.md +497 -0
- data/examples/pay_goodmailer.rb +518 -0
- data/lib/goodmail/action_mailer_integration.rb +296 -0
- data/lib/goodmail/builder.rb +304 -15
- data/lib/goodmail/configuration.rb +40 -3
- data/lib/goodmail/dispatcher.rb +41 -42
- data/lib/goodmail/email.rb +82 -46
- data/lib/goodmail/layout.erb +4 -5
- data/lib/goodmail/layout.rb +1 -1
- data/lib/goodmail/mailer.rb +19 -44
- data/lib/goodmail/plaintext.rb +236 -0
- data/lib/goodmail/version.rb +1 -1
- data/lib/goodmail.rb +8 -4
- metadata +22 -29
|
@@ -4,6 +4,8 @@ require "ostruct"
|
|
|
4
4
|
module Goodmail
|
|
5
5
|
# Handles configuration settings for the Goodmail gem.
|
|
6
6
|
module Configuration
|
|
7
|
+
THREAD_CONFIG_KEY = :goodmail_current_config
|
|
8
|
+
|
|
7
9
|
# Default configuration values
|
|
8
10
|
DEFAULT_CONFIG = OpenStruct.new(
|
|
9
11
|
brand_color: "#348eda",
|
|
@@ -29,22 +31,57 @@ module Goodmail
|
|
|
29
31
|
# Provides the configuration block helper.
|
|
30
32
|
# Ensures validation runs after the block is executed.
|
|
31
33
|
def configure
|
|
32
|
-
yield
|
|
33
|
-
validate_config!(
|
|
34
|
+
yield global_config # Ensures global config is initialized via accessor
|
|
35
|
+
validate_config!(global_config)
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
# Returns the current configuration object.
|
|
37
39
|
# Initializes with a copy of the defaults if not already configured.
|
|
38
40
|
def config
|
|
41
|
+
Thread.current[THREAD_CONFIG_KEY] || global_config
|
|
42
|
+
end
|
|
43
|
+
alias_method :configuration, :config
|
|
44
|
+
|
|
45
|
+
# Runs a block with a per-render configuration override in the current
|
|
46
|
+
# thread. This keeps whitelabel / tenant-specific emails from mutating the
|
|
47
|
+
# process-wide config while preserving the existing `Goodmail.config` read
|
|
48
|
+
# path used by Builder, Layout, and Plaintext.
|
|
49
|
+
#
|
|
50
|
+
# Source: Ruby thread-local variables:
|
|
51
|
+
# https://docs.ruby-lang.org/en/3.4/Thread.html#method-i-5B-5D
|
|
52
|
+
def with_config(overrides)
|
|
53
|
+
return yield if overrides.nil?
|
|
54
|
+
|
|
55
|
+
previous_config = Thread.current[THREAD_CONFIG_KEY]
|
|
56
|
+
Thread.current[THREAD_CONFIG_KEY] = config_with(overrides)
|
|
57
|
+
yield
|
|
58
|
+
ensure
|
|
59
|
+
Thread.current[THREAD_CONFIG_KEY] = previous_config if defined?(previous_config)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def config_with(overrides)
|
|
63
|
+
override_config = config.dup
|
|
64
|
+
if overrides.respond_to?(:to_h)
|
|
65
|
+
overrides.to_h.each { |key, value| override_config[key] = value }
|
|
66
|
+
else
|
|
67
|
+
overrides.each_pair { |key, value| override_config[key] = value }
|
|
68
|
+
end
|
|
69
|
+
validate_config!(override_config)
|
|
70
|
+
override_config
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns the process-wide configuration object, ignoring any temporary
|
|
74
|
+
# per-render override installed by `with_config`.
|
|
75
|
+
def global_config
|
|
39
76
|
@config = DEFAULT_CONFIG.dup unless defined?(@config) && @config
|
|
40
77
|
@config
|
|
41
78
|
end
|
|
42
|
-
alias_method :configuration, :config
|
|
43
79
|
|
|
44
80
|
# Resets the configuration back to the default values.
|
|
45
81
|
# Primarily useful for testing environments.
|
|
46
82
|
def reset_config!
|
|
47
83
|
@config = nil
|
|
84
|
+
Thread.current[THREAD_CONFIG_KEY] = nil
|
|
48
85
|
end
|
|
49
86
|
|
|
50
87
|
private
|
data/lib/goodmail/dispatcher.rb
CHANGED
|
@@ -1,60 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "action_mailer"
|
|
3
|
-
|
|
4
|
-
require_relative "mailer" # Require the internal mailer
|
|
3
|
+
require_relative "mailer"
|
|
5
4
|
|
|
6
5
|
module Goodmail
|
|
7
|
-
# Responsible for orchestrating the building of the
|
|
6
|
+
# Responsible for orchestrating the building of the Action Mailer delivery.
|
|
8
7
|
module Dispatcher
|
|
9
8
|
extend self
|
|
10
9
|
|
|
11
|
-
# Builds
|
|
12
|
-
# an ActionMailer::MessageDelivery object.
|
|
10
|
+
# Builds an ActionMailer::MessageDelivery with HTML and text parts.
|
|
13
11
|
# @api private
|
|
14
12
|
def build_message(headers, &block)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# 7. Build the mail object via the internal Mailer class action.
|
|
39
|
-
delivery_object = Goodmail::Mailer.compose_message(
|
|
40
|
-
mailer_headers,
|
|
41
|
-
raw_html_body,
|
|
42
|
-
nil, # Pass nil for raw_text_body - Premailer generates it
|
|
43
|
-
unsubscribe_url
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
# 8. Return the ActionMailer::MessageDelivery object
|
|
47
|
-
delivery_object
|
|
13
|
+
headers = headers.dup
|
|
14
|
+
render_config_overrides = render_config(headers)
|
|
15
|
+
|
|
16
|
+
Goodmail.with_config(render_config_overrides) do
|
|
17
|
+
parts = Goodmail.render(headers, &block)
|
|
18
|
+
|
|
19
|
+
# ActionMailer::MessageDelivery processes this mailer action lazily and
|
|
20
|
+
# `deliver_later` serializes only action arguments, so pass already
|
|
21
|
+
# rendered strings and plain attachment descriptor hashes into the action.
|
|
22
|
+
# Sources:
|
|
23
|
+
# - MessageDelivery laziness:
|
|
24
|
+
# https://github.com/rails/rails/blob/debbd18c562df17d01944c475e9291d927910b58/actionmailer/lib/action_mailer/message_delivery.rb#L22-L35
|
|
25
|
+
# - deliver_later serializes mailer action arguments:
|
|
26
|
+
# https://github.com/rails/rails/blob/debbd18c562df17d01944c475e9291d927910b58/actionmailer/lib/action_mailer/message_delivery.rb#L142-L155
|
|
27
|
+
Goodmail::Mailer.compose_message(
|
|
28
|
+
slice_mail_headers(headers),
|
|
29
|
+
parts.html,
|
|
30
|
+
parts.text,
|
|
31
|
+
resolved_unsubscribe_url(headers),
|
|
32
|
+
parts.attachments
|
|
33
|
+
)
|
|
34
|
+
end
|
|
48
35
|
end
|
|
49
36
|
|
|
50
37
|
private
|
|
51
38
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
39
|
+
# Pass Action Mailer's normal header surface through, excluding only
|
|
40
|
+
# Goodmail render-only options such as :unsubscribe_url and :preheader.
|
|
54
41
|
def slice_mail_headers(h)
|
|
55
|
-
|
|
42
|
+
Goodmail.action_mailer_headers(h)
|
|
56
43
|
end
|
|
57
44
|
|
|
58
|
-
|
|
45
|
+
def render_config(headers)
|
|
46
|
+
render_option(headers, :config) || render_option(headers, :configuration)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resolved_unsubscribe_url(headers)
|
|
50
|
+
render_option(headers, :unsubscribe_url) || Goodmail.config.unsubscribe_url
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_option(headers, key)
|
|
54
|
+
return headers[key] if headers.key?(key)
|
|
55
|
+
|
|
56
|
+
headers[key.to_s] if headers.key?(key.to_s)
|
|
57
|
+
end
|
|
59
58
|
end
|
|
60
59
|
end
|
data/lib/goodmail/email.rb
CHANGED
|
@@ -3,69 +3,105 @@ require "premailer"
|
|
|
3
3
|
require "cgi" # For unescaping HTML in plaintext generation (though Premailer might handle most)
|
|
4
4
|
|
|
5
5
|
module Goodmail
|
|
6
|
-
# Simple struct to hold the rendered HTML and text parts of an email
|
|
7
|
-
|
|
6
|
+
# Simple struct to hold the rendered HTML and text parts of an email, plus
|
|
7
|
+
# any attachments collected via the `attach` / `inline_image` DSL helpers.
|
|
8
|
+
# Custom Action Mailer classes can call Goodmail's auto-installed
|
|
9
|
+
# `goodmail_mail_parts(parts, headers)` helper to apply attachments, pin
|
|
10
|
+
# inline Content-IDs, add unsubscribe headers, and send the multipart body.
|
|
11
|
+
#
|
|
12
|
+
# `attachments` defaults to `[]` for backwards compatibility — callers
|
|
13
|
+
# written against 0.3.x keep working unchanged.
|
|
14
|
+
EmailParts = Struct.new(:html, :text, :attachments, keyword_init: true) do
|
|
15
|
+
def initialize(html: nil, text: nil, attachments: [])
|
|
16
|
+
super(html: html, text: text, attachments: attachments || [])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
8
19
|
|
|
9
20
|
# Renders the email content using the Goodmail DSL and returns HTML and text parts.
|
|
10
21
|
# This method does not send the email but prepares its content for sending.
|
|
11
22
|
#
|
|
12
23
|
# @param headers [Hash] Mail headers. Expected to contain :subject.
|
|
13
|
-
# Can also contain :unsubscribe_url
|
|
24
|
+
# Can also contain :unsubscribe_url, :preheader, :locale,
|
|
25
|
+
# :context, :config, and :layout_path to override
|
|
26
|
+
# render-only behavior.
|
|
14
27
|
# @param dsl_block [Proc] Block containing Goodmail DSL calls (text, button, etc.)
|
|
15
28
|
# @return [Goodmail::EmailParts] An object containing the :html and :text email parts.
|
|
16
29
|
def self.render(headers = {}, &dsl_block)
|
|
17
30
|
# 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
31
|
current_headers = headers.dup # Avoid modifying the original headers hash directly
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
context = render_header_value!(current_headers, :context)
|
|
33
|
+
locale = render_header_value!(current_headers, :locale)
|
|
34
|
+
render_config = render_header_value!(current_headers, :config)
|
|
35
|
+
render_config = render_header_value!(current_headers, :configuration) if render_config.nil?
|
|
36
|
+
layout_path = render_header_value!(current_headers, :layout_path)
|
|
37
|
+
subject = render_header_value!(current_headers, :subject)
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
core_html_content,
|
|
33
|
-
current_headers[:subject], # Use subject from (potentially modified) current_headers
|
|
34
|
-
unsubscribe_url: unsubscribe_url,
|
|
35
|
-
preheader: preheader
|
|
36
|
-
)
|
|
39
|
+
Goodmail.with_config(render_config) do
|
|
40
|
+
builder = Goodmail::Builder.new(context: context)
|
|
41
|
+
evaluate_builder_dsl(builder, locale, &dsl_block)
|
|
42
|
+
core_html_content = builder.html_output
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
)
|
|
44
|
+
# 2. Determine unsubscribe_url and preheader
|
|
45
|
+
# These are removed from headers as they are Goodmail-specific, not standard mail headers.
|
|
46
|
+
unsubscribe_url = render_header_value!(current_headers, :unsubscribe_url) || Goodmail.config.unsubscribe_url
|
|
47
|
+
preheader = render_header_value!(current_headers, :preheader) || Goodmail.config.default_preheader || subject
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
# 3. Render the raw HTML body using the Layout
|
|
50
|
+
# The subject is passed for the <title> tag and potentially other uses in layout.
|
|
51
|
+
# Unsubscribe URL and preheader are passed for inclusion in the layout.
|
|
52
|
+
raw_html_body = Goodmail::Layout.render(
|
|
53
|
+
core_html_content,
|
|
54
|
+
subject,
|
|
55
|
+
layout_path: layout_path,
|
|
56
|
+
unsubscribe_url: unsubscribe_url,
|
|
57
|
+
preheader: preheader
|
|
58
|
+
)
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
# 4. Run Premailer for CSS inlining (HTML part). Plaintext goes
|
|
61
|
+
# through `Goodmail::Plaintext` which pre-processes the source
|
|
62
|
+
# HTML to neutralize MSO-only markup and the hidden preheader
|
|
63
|
+
# span — both of which Premailer's plaintext extractor would
|
|
64
|
+
# otherwise leak into the text body.
|
|
65
|
+
premailer = Premailer.new(
|
|
66
|
+
raw_html_body,
|
|
67
|
+
with_html_string: true,
|
|
68
|
+
adapter: :nokogiri,
|
|
69
|
+
preserve_styles: false, # Force inlining and remove <style> block
|
|
70
|
+
remove_ids: true, # Remove IDs
|
|
71
|
+
remove_comments: false, # Keep MSO conditional comments in HTML
|
|
72
|
+
input_encoding: "UTF-8" # See Goodmail::Plaintext for the full
|
|
73
|
+
# rationale — short version: Premailer
|
|
74
|
+
# double-encodes accented characters when
|
|
75
|
+
# the source has no <meta charset>.
|
|
76
|
+
)
|
|
77
|
+
final_inlined_html = premailer.to_inline_css
|
|
78
|
+
final_plain_text = Goodmail::Plaintext.generate(raw_html_body, preheader: preheader)
|
|
79
|
+
|
|
80
|
+
# 5. Return the structured parts
|
|
81
|
+
EmailParts.new(
|
|
82
|
+
html: final_inlined_html,
|
|
83
|
+
text: final_plain_text,
|
|
84
|
+
attachments: builder.attachments
|
|
85
|
+
)
|
|
59
86
|
end
|
|
87
|
+
end
|
|
60
88
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
89
|
+
def self.render_header_value!(headers, key)
|
|
90
|
+
value = headers.delete(key)
|
|
91
|
+
value = headers.delete(key.to_s) if value.nil? && headers.key?(key.to_s)
|
|
92
|
+
value
|
|
93
|
+
end
|
|
94
|
+
private_class_method :render_header_value!
|
|
64
95
|
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
def self.evaluate_builder_dsl(builder, locale, &dsl_block)
|
|
97
|
+
return unless block_given?
|
|
67
98
|
|
|
68
|
-
|
|
69
|
-
|
|
99
|
+
render_block = proc { builder.instance_eval(&dsl_block) }
|
|
100
|
+
if !locale.nil? && !locale.to_s.empty? && defined?(I18n)
|
|
101
|
+
I18n.with_locale(locale, &render_block)
|
|
102
|
+
else
|
|
103
|
+
render_block.call
|
|
104
|
+
end
|
|
70
105
|
end
|
|
106
|
+
private_class_method :evaluate_builder_dsl
|
|
71
107
|
end
|
data/lib/goodmail/layout.erb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta name="viewport" content="width=device-width" />
|
|
5
5
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
6
|
-
<title><%= subject %></title>
|
|
6
|
+
<title><%= ERB::Util.html_escape(subject) %></title>
|
|
7
7
|
<style type="text/css">
|
|
8
8
|
/* Base Styles */
|
|
9
9
|
img {
|
|
@@ -107,7 +107,6 @@
|
|
|
107
107
|
cursor: pointer;
|
|
108
108
|
display: inline-block;
|
|
109
109
|
border-radius: 5px;
|
|
110
|
-
text-transform: capitalize;
|
|
111
110
|
background-color: <%= config.brand_color %>;
|
|
112
111
|
margin: 0;
|
|
113
112
|
border-color: <%= config.brand_color %>;
|
|
@@ -163,7 +162,7 @@
|
|
|
163
162
|
|
|
164
163
|
<!-- Hidden Preheader Text -->
|
|
165
164
|
<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 %>
|
|
165
|
+
<%= ERB::Util.html_escape(preheader || subject) %>
|
|
167
166
|
</span>
|
|
168
167
|
|
|
169
168
|
<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">
|
|
@@ -193,7 +192,7 @@
|
|
|
193
192
|
<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">
|
|
194
193
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
|
195
194
|
<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 %>" />
|
|
195
|
+
<meta itemprop="name" content="<%= ERB::Util.html_escape(subject) %>" />
|
|
197
196
|
<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;">
|
|
198
197
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
|
199
198
|
<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top">
|
|
@@ -229,7 +228,7 @@
|
|
|
229
228
|
<% if config.show_footer_unsubscribe_link && defined?(unsubscribe_url) && unsubscribe_url.present? %>
|
|
230
229
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
|
231
230
|
<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">
|
|
232
|
-
<a href="<%= unsubscribe_url %>" style="color: #999;"><%= config.footer_unsubscribe_link_text %></a>
|
|
231
|
+
<a href="<%= ERB::Util.html_escape(unsubscribe_url) %>" style="color: #999;"><%= config.footer_unsubscribe_link_text %></a>
|
|
233
232
|
</td>
|
|
234
233
|
</tr>
|
|
235
234
|
<% end %>
|
data/lib/goodmail/layout.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Goodmail
|
|
|
32
32
|
body_html: body_html,
|
|
33
33
|
subject: subject || "",
|
|
34
34
|
config: Goodmail.config, # Make config available
|
|
35
|
-
unsubscribe_url: unsubscribe_url,
|
|
35
|
+
unsubscribe_url: unsubscribe_url, # Pass unsubscribe URL to template
|
|
36
36
|
preheader: preheader # Pass preheader to template
|
|
37
37
|
)
|
|
38
38
|
rescue => e
|
data/lib/goodmail/mailer.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "action_mailer"
|
|
3
|
-
require "premailer" # Require premailer library
|
|
4
3
|
|
|
5
4
|
module Goodmail
|
|
6
5
|
# Internal Mailer class.
|
|
@@ -16,53 +15,29 @@ module Goodmail
|
|
|
16
15
|
# This instance method acts as the mailer action.
|
|
17
16
|
# It's called via Goodmail::Mailer.compose_message(...)
|
|
18
17
|
# Action Mailer wraps the result in a MessageDelivery object.
|
|
19
|
-
# It uses Premailer to inline CSS and generate plaintext.
|
|
20
18
|
# @api internal
|
|
21
|
-
def compose_message(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# Note: `plain_text_images: false` might exist but is not standard;
|
|
32
|
-
# relying on gsub cleanup below.
|
|
33
|
-
)
|
|
19
|
+
def compose_message(
|
|
20
|
+
headers,
|
|
21
|
+
html_body,
|
|
22
|
+
text_body,
|
|
23
|
+
unsubscribe_url,
|
|
24
|
+
dsl_attachments = []
|
|
25
|
+
)
|
|
26
|
+
headers = headers.dup
|
|
27
|
+
goodmail_add_list_unsubscribe_headers!(headers, unsubscribe_url)
|
|
28
|
+
goodmail_apply_attachments!(dsl_attachments)
|
|
34
29
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
#
|
|
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
|
-
|
|
53
|
-
# Add List-Unsubscribe header to the headers hash *before* calling mail()
|
|
54
|
-
if unsubscribe_url.is_a?(String) && !unsubscribe_url.strip.empty?
|
|
55
|
-
headers["List-Unsubscribe"] = "<#{unsubscribe_url.strip}>"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Call the instance-level `mail` method
|
|
30
|
+
# Attachments must be registered before `mail` materializes the message;
|
|
31
|
+
# the block form below lets Rails build the multipart/alternative tree.
|
|
32
|
+
# Sources:
|
|
33
|
+
# - late attachments raise:
|
|
34
|
+
# https://github.com/rails/rails/blob/debbd18c562df17d01944c475e9291d927910b58/actionmailer/lib/action_mailer/base.rb#L766-L781
|
|
35
|
+
# - block-form `mail` with direct plain/html rendering:
|
|
36
|
+
# https://github.com/rails/rails/blob/debbd18c562df17d01944c475e9291d927910b58/actionmailer/lib/action_mailer/base.rb#L851-L873
|
|
59
37
|
mail(headers) do |format|
|
|
60
|
-
|
|
61
|
-
format.
|
|
62
|
-
# Use the CSS-inlined HTML
|
|
63
|
-
format.html { render html: inlined_html.html_safe }
|
|
38
|
+
format.text { render plain: text_body.to_s }
|
|
39
|
+
format.html { render html: html_body.to_s.html_safe }
|
|
64
40
|
end
|
|
65
|
-
# Action Mailer automatically returns the MessageDelivery object
|
|
66
41
|
end
|
|
67
42
|
end
|
|
68
43
|
end
|