Module: Goodmail

Extended by:
Configuration
Defined in:
lib/goodmail.rb,
lib/goodmail/email.rb,
lib/goodmail/error.rb,
lib/goodmail/layout.rb,
lib/goodmail/mailer.rb,
lib/goodmail/builder.rb,
lib/goodmail/version.rb,
lib/goodmail/plaintext.rb,
lib/goodmail/dispatcher.rb,
lib/goodmail/configuration.rb,
lib/goodmail/action_mailer_integration.rb

Overview

The main namespace for the Goodmail gem. Provides configuration and the primary .compose method.

Defined Under Namespace

Modules: ActionMailerIntegration, Configuration, Dispatcher, Layout, Plaintext Classes: Builder, EmailParts, Error, Mailer

Constant Summary collapse

VERSION =
"0.4.0"
LIST_UNSUBSCRIBE_HEADER =
"List-Unsubscribe"
LIST_UNSUBSCRIBE_POST_HEADER =
"List-Unsubscribe-Post"
LIST_UNSUBSCRIBE_ONE_CLICK_VALUE =
"List-Unsubscribe=One-Click"
GOODMAIL_RENDER_HEADER_KEYS =
%i[
  preheader
  unsubscribe_url
  locale
  context
  config
  configuration
  layout_path
].freeze

Constants included from Configuration

Configuration::DEFAULT_CONFIG, Configuration::REQUIRED_CONFIG_KEYS, Configuration::THREAD_CONFIG_KEY

Class Method Summary collapse

Methods included from Configuration

config, config_with, configure, global_config, reset_config!, with_config

Class Method Details

.action_mailer_headers(headers) ⇒ Object

Returns the header hash Goodmail should hand to Action Mailer’s ‘mail`. Rails intentionally accepts arbitrary message headers and filters only framework-only render keys internally; Goodmail should follow that shape instead of maintaining a narrow whitelist of envelope fields. Source: github.com/rails/rails/blob/debbd18c562df17d01944c475e9291d927910b58/actionmailer/lib/action_mailer/base.rb#L972-L976



59
60
61
62
63
64
65
# File 'lib/goodmail/action_mailer_integration.rb', line 59

def self.action_mailer_headers(headers)
  headers.each_with_object({}) do |(key, value), result|
    next if render_header_key?(key)

    result[key] = value
  end
end

.compose(headers = {}, &block) ⇒ ActionMailer::MessageDelivery

Composes an ActionMailer::MessageDelivery using the Goodmail DSL and layout.

This is the primary entry point for creating emails with Goodmail. The returned MessageDelivery can then have ‘.deliver_now` or `.deliver_later` called on it.

Examples:

mail = Goodmail.compose(to: 'user@example.com', subject: 'Hello!') do
  text "This is the email body."
  button "Click Me", "https://example.com"
end

mail.deliver_now
# or
mail.deliver_later

Parameters:

  • headers (Hash) (defaults to: {})

    Mail headers (:to, :from, :subject, etc.) Also accepts :unsubscribe (true or String URL).

  • block (Proc)

    Block containing Goodmail DSL calls (text, button, etc.)

Returns:

  • (ActionMailer::MessageDelivery)

    The generated message delivery, ready for delivery.



48
49
50
51
# File 'lib/goodmail.rb', line 48

def self.compose(headers = {}, &block)
  # Delegate the actual building process to the Dispatcher
  Dispatcher.build_message(headers, &block)
end

.install_action_mailer_integration!(base = ActionMailer::Base) ⇒ Object

Installs Goodmail’s mailer helpers once on ActionMailer::Base so app mailers, Devise mailers, Pay mailers, and other custom Action Mailer subclasses can call ‘goodmail_mail` without per-class include glue.

The helper methods stay private because Action Mailer dispatches mailer actions through ‘action_methods`; keeping the API private prevents Rails from treating helpers as deliverable actions. Source: github.com/rails/rails/blob/debbd18c562df17d01944c475e9291d927910b58/actionmailer/lib/action_mailer/base.rb#L614-L618



81
82
83
84
85
# File 'lib/goodmail/action_mailer_integration.rb', line 81

def self.install_action_mailer_integration!(base = ActionMailer::Base)
  return if base < ActionMailerIntegration

  base.include(ActionMailerIntegration)
end

.list_unsubscribe_headers(unsubscribe_url) ⇒ Object

Returns the deliverability headers for a configured unsubscribe URL.

RFC 8058 one-click unsubscribe is only eligible for HTTPS List-Unsubscribe URLs and uses the exact ‘List-Unsubscribe=One-Click` POST body. Keep the classic List-Unsubscribe header for other non-blank values, but don’t advertise one-click POST support unless the URL qualifies.

Sources:

- RFC 8058 §3.1:
  https://www.rfc-editor.org/rfc/rfc8058#section-3.1
- Gmail sender guidelines:
  https://support.google.com/mail/answer/81126
- Yahoo sender best practices:
  https://senders.yahooinc.com/best-practices/


33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/goodmail/action_mailer_integration.rb', line 33

def self.list_unsubscribe_headers(unsubscribe_url)
  return {} unless unsubscribe_url.is_a?(String)

  stripped_url = unsubscribe_url.strip
  return {} if stripped_url.empty?

  headers = { LIST_UNSUBSCRIBE_HEADER => "<#{stripped_url}>" }
  if one_click_unsubscribe_url?(stripped_url)
    headers[LIST_UNSUBSCRIBE_POST_HEADER] = LIST_UNSUBSCRIBE_ONE_CLICK_VALUE
  end
  headers
end

.one_click_unsubscribe_url?(url) ⇒ Boolean

Returns:

  • (Boolean)


46
47
48
49
50
51
# File 'lib/goodmail/action_mailer_integration.rb', line 46

def self.one_click_unsubscribe_url?(url)
  uri = URI.parse(url)
  uri.is_a?(URI::HTTPS) && !uri.host.to_s.empty?
rescue URI::InvalidURIError
  false
end

.render(headers = {}, &dsl_block) ⇒ Goodmail::EmailParts

Renders the email content using the Goodmail DSL and returns HTML and text parts. This method does not send the email but prepares its content for sending.

Parameters:

  • headers (Hash) (defaults to: {})

    Mail headers. Expected to contain :subject. Can also contain :unsubscribe_url, :preheader, :locale, :context, :config, and :layout_path to override render-only behavior.

  • dsl_block (Proc)

    Block containing Goodmail DSL calls (text, button, etc.)

Returns:



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/goodmail/email.rb', line 29

def self.render(headers = {}, &dsl_block)
  # 1. Initialize the Builder and execute the DSL block
  current_headers = headers.dup # Avoid modifying the original headers hash directly
  context = render_header_value!(current_headers, :context)
  locale = render_header_value!(current_headers, :locale)
  render_config = render_header_value!(current_headers, :config)
  render_config = render_header_value!(current_headers, :configuration) if render_config.nil?
  layout_path = render_header_value!(current_headers, :layout_path)
  subject = render_header_value!(current_headers, :subject)

  Goodmail.with_config(render_config) do
    builder = Goodmail::Builder.new(context: context)
    evaluate_builder_dsl(builder, locale, &dsl_block)
    core_html_content = builder.html_output

    # 2. Determine unsubscribe_url and preheader
    #    These are removed from headers as they are Goodmail-specific, not standard mail headers.
    unsubscribe_url = render_header_value!(current_headers, :unsubscribe_url) || Goodmail.config.unsubscribe_url
    preheader = render_header_value!(current_headers, :preheader) || Goodmail.config.default_preheader || subject

    # 3. Render the raw HTML body using the Layout
    #    The subject is passed for the <title> tag and potentially other uses in layout.
    #    Unsubscribe URL and preheader are passed for inclusion in the layout.
    raw_html_body = Goodmail::Layout.render(
      core_html_content,
      subject,
      layout_path: layout_path,
      unsubscribe_url: unsubscribe_url,
      preheader: preheader
    )

    # 4. Run Premailer for CSS inlining (HTML part). Plaintext goes
    #    through `Goodmail::Plaintext` which pre-processes the source
    #    HTML to neutralize MSO-only markup and the hidden preheader
    #    span — both of which Premailer's plaintext extractor would
    #    otherwise leak into the text body.
    premailer = Premailer.new(
      raw_html_body,
      with_html_string: true,
      adapter: :nokogiri,
      preserve_styles: false, # Force inlining and remove <style> block
      remove_ids: true,       # Remove IDs
      remove_comments: false, # Keep MSO conditional comments in HTML
      input_encoding: "UTF-8" # See Goodmail::Plaintext for the full
                              # rationale — short version: Premailer
                              # double-encodes accented characters when
                              # the source has no <meta charset>.
    )
    final_inlined_html = premailer.to_inline_css
    final_plain_text = Goodmail::Plaintext.generate(raw_html_body, preheader: preheader)

    # 5. Return the structured parts
    EmailParts.new(
      html: final_inlined_html,
      text: final_plain_text,
      attachments: builder.attachments
    )
  end
end

.render_header_key?(key) ⇒ Boolean

Returns:

  • (Boolean)


67
68
69
70
# File 'lib/goodmail/action_mailer_integration.rb', line 67

def self.render_header_key?(key)
  GOODMAIL_RENDER_HEADER_KEYS.include?(key) ||
    (key.is_a?(String) && GOODMAIL_RENDER_HEADER_KEYS.include?(key.to_sym))
end