Reputation: 8247
I am using an after_action
callback in my mailers to record that email was sent. The emails are sent through Delayed Job. This works, except when we are unable to reach the remote server - in which case, the email is not sent, but we record that it was. Delayed Job retries the email later, and it is successfully delivered, but we've then recorded that two emails were sent.
It looks something like this:
class UserMailer < ActionMailer::Base
after_action :record_email
def record_email
Rails.logger.info("XYZZY: Recording Email")
@user.emails.create!
end
def spam!(user)
@user = user
Rails.logger.info("XYZZY: Sending spam!")
m = mail(to: user.email, subject: 'SPAM!')
Rails.logger.info("XYZZY: mail method finished")
m
end
end
I call this code like this (using delayed job performable mailer):
UserMailer.delay.spam!( User.find(1))
When I step through this in a debugger, it seems that my after_action method is called before the mail is delivered.
[Job:104580969] XYZZY: Sending spam!
[Job:104580969] XYZZY: mail method finished
[Job:104580969] XYZZY: Recording Email
Job UserMailer.app_registration_welcome (id=104580969) FAILED (3 prior attempts) with Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 1025
How can I catch network errors in my mailer methods and record that the email attempt failed, or do nothing at all? I'm using Rails 4.2.4.
Upvotes: 9
Views: 3922
Reputation: 15248
In rails 7.1+ there are before_deliver
, around_deliver
, after_deliver
callbacks
class MyMailer < ApplicationMailer
after_deliver :make_something_after_deliver
def send_mail
mail to: "[email protected]", subject: "Email"
end
private
def make_something_after_deliver
# make something after deliver
end
end
Upvotes: 1
Reputation: 1313
Callbacks after successful delivery are called observers in action mailer and available since rails 3.x.
From https://guides.rubyonrails.org/action_mailer_basics.html#observing-emails :
An observer class must implement the :delivered_email(message) method, which will be called after the email is sent.
class EmailDeliveryObserver
def self.delivered_email(message)
EmailDelivery.log(message)
end
end
Similar to interceptors, you must register observers using the observers config option. You can do this in an initializer file like config/initializers/mail_observers.rb:
Rails.application.configure do
config.action_mailer.observers = %w[EmailDeliveryObserver]
end
There is also a register_observer
class method to add an observer only to a specific mailer.
Upvotes: 0
Reputation: 15934
The debug messages you showed make perfect sense - the mailer action finishes immediately because the mailing action itself is asynchronous, handled by Delayed job in a completely different process. So there is no way the mailer class itself can know how the mailing action finished.
What I think you need instead is implementing Delayed job hooks. You'd have to rewrite your mailers and the calls to send emails a bit though.
I have not tested it fully but something along the following lines should work:
class MailerJob
def initialize(mailer_class, mailer_action, recipient, *params)
@mailer_class = mailer_class
@mailer_action = mailer_action
@recipient = recipient
@params = params
end
def perform
@mailer_class.send(@mailer_action, @recipient, *@params)
end
def success(job)
Rails.logger.debug "recording email!"
@recipient.emails.create!
end
def failure(job)
Rails.logger.debug "sending email to #{@recipient.email} failed!"
end
end
MailerJob
is a custom job to be run by Delayed job. I tried to make it as general as possible, so it accepts the mailer class, the mailer action, the recipient (typically the user) and other optional params. Also it requires the recipient
to have the emails
association.
The job has two hooks defined: success
when the mailing action succeeds which creates the email
record in the database and another one for logging failure. The actual sending is done in the perform
method. Note that inside it the delayed
method is not used as the whole job is already enqueued in a background Delayed job queue when being called.
To send a mail using this custom job you have to enqueue it to Delayed job, e.g.:
Delayed::Job.enqueue MailerJob.new(UserMailer, :spam!, User.find(1))
Upvotes: 0
Reputation: 8247
This is what I came up with, I would love to have a better way.
I used the Mail delivery callback:
delivery_callback.rb
class DeliveryCallback
def delivered_email(mail)
data = mail.instance_variable_get(:@_callback_data)
unless data.nil?
data[:user].email.create!
end
end
end
config/initializes/mail.rb
Mail.register_observer( DeliveryCallback.new )
And I replaced my record_email method:
class UserMailer < ActionMailer::Base
after_action :record_email
def record_email
@_message.instance_variable_set(:@_callback_data, {:user => user})
end
end
This seems to work, if the remote server is not available, the delivered_email callback is not invoked.
Is there a better way!?!?
Upvotes: 4
Reputation: 6438
Try the following:
class UserMailer < ActionMailer::Base
# after_action :record_email
def record_email
Rails.logger.info("XYZZY: Recording Email")
@user.emails.create!
end
def spam!(user)
begin
@user = user
Rails.logger.info("XYZZY: Sending spam!")
m = mail(to: user.email, subject: 'SPAM!')
Rails.logger.info("XYZZY: mail method finished")
m
rescue Errno::ECONNREFUSED
record_email
end
end
end
Upvotes: -1