Store mailer class and method after email sent in rails

Not sure exactly what you're asking here ... you want to track when the Mailer is used or where it's used from?

If it's the former, you could hook into method calls with something like: https://gist.github.com/ridiculous/783cf3686c51341ba32f

If it's the latter, then the only way I can think of is using __callee__ to get that info.

Hope that helps!


Just to illuminate a simpler answer that is hinted at by Mark Murphy's first comment, I went with a very simple approach like this:

class ApplicationMailer < ActionMailer::Base
  default from: "[email protected]"
  after_action :log_email

  private
  def log_email
    mailer_class = self.class.to_s
    mailer_action = self.action_name
    EmailLog.log("#{mailer_class}##{mailer_action}", message)
  end
end

With a simple model EmailLog to save the record

class EmailLog < ApplicationRecord

  def self.log(email_type, message)
    EmailLog.create(
      email_type: email_type,
      from: self.comma_separated(message.from),
      to: self.comma_separated(message.to),
      cc: self.comma_separated(message.cc),
      subject: message.subject,
      body: message.body)
  end

  private
  def self.comma_separated(arr)
    if !arr.nil? && !arr.empty?
      arr.join(",")
    else
      ""
    end
  end

end

If all of your mailers derive from ApplicationMailer, then you're all set.


You can use process_action callback (why not?) to intercept mailer arguments, eg.:

class BaseMailer < ActionMailer::Base
  private

  # see https://api.rubyonrails.org/classes/AbstractController/Callbacks.html#method-i-process_action
  def process_action(action_name, *action_args)
    super

    track_email_status(action_name, action_args)
  end

  # move these methods to the concern, copied here for the sake of simplicity!
  def track_email_status(action_name, action_args)
    email_status = EmailStatus.create!(
      user: (action_args.first if action_args.first.is_a?(User)),
      email: message.to.first,
      mailer_kind: "#{self.class.name}##{action_name}",
      mailer_args: tracked_mailer_args(action_name, action_args)
    )

    message.instance_variable_set(:@email_status, email_status)
  end

  def tracked_mailer_args(action_name, action_args)
    args_map = method(action_name).parameters.map(&:second).zip(action_args).to_h
    args_map = self.class.parameter_filter.filter(args_map)
    simplify_tracked_arg(args_map.values)
  end

  def simplify_tracked_arg(argument)
    case argument
    when Hash then argument.transform_values { |v| simplify_tracked_arg(v) }
    when Array then argument.map { |arg| simplify_tracked_arg(arg) }
    when ActiveRecord::Base then "#{argument.class.name}##{argument.id}"
    else argument
    end
  end

  def self.parameter_filter
    @parameter_filter ||= ActionDispatch::Http::ParameterFilter.new(Rails.application.config.filter_parameters)
  end
end

This way you can track mailer headers/class/action_name/arguments and build sophisticated emails tracking backend. You can also use an Observer to track when email was sent:

class EmailStatusObserver

  def self.delivered_email(mail)
    mail.instance_variable_get(:@email_status)&.touch(:sent_at)
  end

end

# config/initializers/email_status_observer.rb
ActionMailer::Base.register_observer(EmailStatusObserver)

RSpec test:

describe BaseMailer do
  context 'track email status' do
    let(:school) { create(:school) }
    let(:teacher) { create(:teacher, school: school) }
    let(:password) { 'May the Force be with you' }
    let(:current_time) { Time.current.change(usec: 0) }

    around { |example| travel_to(current_time, &example) }

    class TestBaseMailer < BaseMailer
      def test_email(user, password)
        mail to: user.email, body: password
      end
    end

    subject { TestBaseMailer.test_email(teacher, password) }

    it 'creates EmailStatus with tracking data' do
      expect { subject.deliver_now }.to change { EmailStatus.count }.by(1)

      email_status = EmailStatus.last
      expect(email_status.user_id).to eq(teacher.id)
      expect(email_status.email).to eq(teacher.email)
      expect(email_status.sent_at).to eq(current_time)
      expect(email_status.status).to eq(:sent)
      expect(email_status.mailer_kind).to eq('TestBaseMailer#test_email')
      expect(email_status.mailer_args).to eq(["Teacher##{teacher.id}", '[FILTERED]'])
    end
  end

end

Here's what I ended up going with... I would love some feedback about the pros and cons of doing it this way. Feels kind of ugly to me but it was easy. Basically, I included the ability to use callbacks in my mailer, attaching the class and method name metadata to the Mail::Message object so that it would be accessible in my observer. I attached it by setting instance variables on the Mail::Message object, and then sending attr_reader to the Mail::Message class, allowing me to call mail.mailer_klass and mail.mailer_action.

I did it this way because I wanted to record the Mail::Message object after it had been delivered so I could get the exact date it had been sent and know that the logged email should have successfully sent.

The mailer:

class MyMailer < ActionMailer::Base
  default from: "[email protected]"

  include AbstractController::Callbacks

  # Where I attach the class and method
  after_action :attach_metadata

  def welcome_note(user)
    @user = user

    mail(subject: "Thanks for signing up!", to: @user.email)
  end

  private

    def attach_metadata
      mailer_klass = self.class.to_s
      mailer_action = self.action_name

      self.message.instance_variable_set(:@mailer_klass, mailer_klass)
      self.message.instance_variable_set(:@mailer_action, mailer_action)

      self.message.class.send(:attr_reader, :mailer_klass)
      self.message.class.send(:attr_reader, :mailer_action)
    end
end

The observer:

class MailAuditor

  def self.delivered_email(mail)
    if mail.multipart?
      body = mail.html_part.decoded
    else
      body = mail.body.raw_source
    end

    Email.create!(
      sender: mail.from,
      recipient: mail.to,
      bcc: mail.bcc,
      cc: mail.cc,
      subject: mail.subject,
      body: body,
      mailer_klass: mail.mailer_klass,
      mailer_action: mail.mailer_action,
      sent_at: mail.date
    )
  end
end

config/initializers/mail.rb

ActionMailer::Base.register_observer(MailAuditor)

Thoughts?