在 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_klass
和 mail.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
我正在努力创建一个数据库支持的电子邮件审计系统,这样我就可以跟踪电子邮件信息。棘手的部分是我希望能够通过邮件程序组织这些 classes 并且还能够存储邮件程序方法的名称。
创建邮件程序拦截器或观察器以从 Mail::Message
实例收集数据并不难,但我很好奇是否有办法捕获创建的 class 和方法名称该消息的实例。
如果可能的话,我宁愿不使用回调。
有什么想法吗?
不确定您在这里问的到底是什么...您想跟踪 Mailer 的使用时间或从何处使用?
如果是前者,您可以使用类似以下内容的方法调用:https://gist.github.com/ridiculous/783cf3686c51341ba32f
如果是后者,那么我能想到的唯一方法就是使用 __callee__
来获取该信息。
希望对您有所帮助!
这就是我最终的结果......我希望得到一些关于这样做的利弊的反馈。对我来说感觉有点丑陋,但这很容易。基本上,我在我的邮件程序中加入了使用回调的功能,将 class 和方法名称元数据附加到 Mail::Message
对象,以便我的观察者可以访问它。我通过在 Mail::Message
对象上设置实例变量来附加它,然后将 attr_reader
发送到 Mail::Message
class,允许我调用 mail.mailer_klass
和 mail.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