When it comes to sending email in Rails, I’ve wondered for years about the gap between this:
class UserMailer < ApplicationMailer def welcome(user) @user = user mail(to: user.email) end end
class User def send_welcome_notification UserMailer.welcome(self).deliver_later end end
We are defining an instance method, but we are calling a class method. What’s going on there? I finally decided to take a closer look.
Well naturally this is implemented by
method_missing. When you call
UserMailer.welcome, the class will call your instance method—sort of! Actually
method_missing just returns a
MessageDelivery object, which provides lazy evaluation. It’s like a promise (but not asynchronous). Your method doesn’t get called until you resolve the “promise,” which normally would happen when you say
deliver_now. You can also call
#message which must resolve the promise (and returns whatever your method returned—sort of!).
What if you say
deliver_later? That still doesn’t call your method. Instead it queues up a job, and later that will say
deliver_now to finally call your method.
But if you’re using Sidekiq (with
config.active_job.queue_adapter = :sidekiq), you might wonder how that
#welcome method works, since we’re passing a
User class and Sidekiq can only serialize primitive types. But it does work! The trick is that Rails’ queue adapter for Sidekiq does its own serialization before handing off the job to Sidekiq, and it tells Sidekiq to run its own Worker subclass that will deserialize things correctly.
All this assumes that your mailer method returns a
Mail::Message instance. That’s what
And if you call
Message. So when you finally call
deliver_now, only one email will go out (ask me how I know).
Btw it turns out this is pretty much all documented on the
ActionMailer::Base class, but it’s not really covered in the Rails Guide, so I never came across it. I only found those docs when I decided to read the code. I don’t know if other Rails devs spend much time reading Rails’ own code, but I’ve found it helpful again and again. It’s not hard and totally worth it!
Another trick I’ve used for years is
bundle show actionmailer (or in the old days
cd $(bundle show actionmailer), before they broke that with a deprecation notice), and then you can add
binding.pry wherever you like. It’s a great way to test your understanding of what’s happening or discover the internals of something.