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
and this:
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 #mail
is giving you. But what if you don’t? What if you call mail
but not as the last line of your method? What if you call it more than once?
Well actually #mail
(linking to the source code this time) remembers the message it generated, so even if you don’t return that from your own method, Rails will still send it properly. In fact it doesn’t matter what your own method returns!
And if you call #mail
multiple times, then Rails will return early and do nothing for the second and third calls—sort of! If you pass any arguments or a block, then Rails will evaluate it again. But it still only knows how to store one 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 pp
or binding.pry
wherever you like. It’s a great way to test your understanding of what’s happening or discover the internals of something.