Append to the subject of every outgoing email (Devise)

I’m wondering if there’s any way to append text to the subject of every email sent by an application. The use case is that I have two copies of an application (production and staging) and would like both to send email, and for it to be clear where that email came from. So subjects could be:

"[Production] Please confirm your email address"
"[Staging] Please confirm your email address"

…and so on. I can do this by overriding the subject of each email individually, but it would be nice if it could be done in one place.

You can do this using a mail interceptor on your staging environment. Start here: ActionMailer::Base and see where it leads you. We do this at work, and it is pretty drop-in-and-forget.

Here’s some more about this solution:

Relevant RailsGuide: Action Mailer Basics — Ruby on Rails Guides

Sample interceptor:

# frozen_string_literal: true

require 'nokogiri'

class CustomMailInterceptor
  def self.delivering_email(message)
    prepend_subject(message)
    append_intended_addresses_to_text_part(message)
    append_intended_addresses_to_html_part(message)
    override_recipients(message)
  end

  def self.append_intended_addresses_to_html_part(message)
    html_part = message.html_part || message
    return unless html_part.content_type.include?('text/html')

    doc = Nokogiri::HTML5.fragment html_part.body.to_s
    doc.add_child '<hr>'
    doc.add_child build_table(intended_addresses(message)).to_s
    html_part.body = doc.to_html
  end

  def self.append_intended_addresses_to_text_part(message)
    text_part = message.text_part || message
    return unless text_part.content_type.include?('text/plain')

    text_part.body = text_part.body.to_s + intended_addresses(message) if text_part.present?
  end

  def self.intended_addresses(message)
    # rubocop:disable Naming/HeredocDelimiterNaming
    <<~EOF
      ----------------
      Intended sender: #{message.from}
      Intended recipient(s): #{message.to}
      Intended cc(s): #{message.cc}
      Intended bcc(s): #{message.bcc}
    EOF
    # rubocop:enable Naming/HeredocDelimiterNaming
  end

  def self.build_table(text)
    text.lines.compact.map do |line|
      parts = line.strip.split(': ')
      next if parts.length < 2

      build_row(parts)
    end.join('').prepend('<table style="border-collapse: collapse">').concat('</table>')
  end

  def self.build_row(parts)
    <<~HTML
      <tr>
        <td style="font-weight: bold; text-align: right; padding-right: 1em">
          #{parts.first}
        </td>
        <td>
          #{parts.second}
        </td>
      </tr>
    HTML
  end

  def self.override_recipients(message)
    message.from = "your-server@your.domain"
    message.to = "you@your.domain"
    message.cc = nil
    message.bcc = nil
  end

  def self.prepend_subject(message)
    message.subject = "[TESTING IN #{Rails.env.upcase}] " + message.subject
  end
end

Thanks, that looks useful - I’ll give it a try. I found this guide as well: Mail Interceptors and Observers for Logging and Development | Leigh Halliday