在 rails 中发送电子邮件后存储邮件程序 class 和方法

Store mailer class and method after email sent in rails

我正在努力创建一个数据库支持的电子邮件审计系统,这样我就可以跟踪电子邮件信息。棘手的部分是我希望能够通过邮件程序组织这些 classes 并且还能够存储邮件程序方法的名称。

创建邮件程序拦截器或观察器以从 Mail::Message 实例收集数据并不难,但我很好奇是否有办法捕获创建的 class 和方法名称该消息的实例。

如果可能的话,我宁愿不使用回调。

有什么想法吗?

不确定您在这里问的到底是什么...您想跟踪 Mailer 的使用时间或从何处使用?

如果是前者,您可以使用类似以下内容的方法调用:https://gist.github.com/ridiculous/783cf3686c51341ba32f

如果是后者,那么我能想到的唯一方法就是使用 __callee__ 来获取该信息。

希望对您有所帮助!

这就是我最终的结果......我希望得到一些关于这样做的利弊的反馈。对我来说感觉有点丑陋,但这很容易。基本上,我在我的邮件程序中加入了使用回调的功能,将 class 和方法名称元数据附加到 Mail::Message 对象,以便我的观察者可以访问它。我通过在 Mail::Message 对象上设置实例变量来附加它,然后将 attr_reader 发送到 Mail::Message class,允许我调用 mail.mailer_klassmail.mailer_action.

我这样做是因为我想在 Mail::Message 对象交付后记录它,这样我就可以获得它发送的确切日期,并且知道记录的电子邮件应该已成功发送。

邮寄者:

class MyMailer < ActionMailer::Base
  default from: "noreply@theapp.com"

  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

观察者:

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)

想法?

我可能会使用 alias_method_chain 类型的方法 - 类似于 Ryan 的方法,但不使用 eval

https://gist.github.com/kenmazaika/cd80b0379655d39690d8

从来都不是观察家的忠实粉丝。

为了阐明 Mark Murphy 的第一条评论所暗示的更简单的答案,我采用了一种非常简单的方法,如下所示:

class ApplicationMailer < ActionMailer::Base
  default from: "noreply@theapp.com"
  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

用一个简单的模型EmailLog来保存记录

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

如果您的所有邮件程序都派生自 ApplicationMailer,那么您就大功告成了。

您可以使用 process_action callback(为什么不呢?)拦截邮件参数,例如:

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

通过这种方式,您可以跟踪邮件程序 headers/class/action_name/arguments 并构建复杂的电子邮件跟踪后端。您还可以使用 Observer 来跟踪电子邮件的发送时间:

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 测试:

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