Skip to main content

27 Forum Series Part 5: Email Notifications with Rails 4.2, deliver_later, and Previews

Episode 29 · October 23, 2014

Learn how to send email notifications to users with Rails 4.2 ActionMailer, deliver_later, and email previews

Emails


Transcripts

Now over the course of the last few episodes, we've been building a forum, and we've created forum threads and forum posts and all the interactions that you typically see with that, however, we now need to start adding things like email notifications. So in this episode we're going to talk about adding email notifications any time you submit a new comment and then how to send this in the background as a background job as well as how to preview these from inside of Rails itself without sending any actual emails. So let's dive in.

Looking at the Rails guides for Action Mailer we can find the deliver_later method, and we can see that inside the Rails guides, it recommends using deliver_later, but it also has this deliver_now method. So there's two different ways that we can be sending emails now in Rails 4.2, and previously we just had the .deliver on an email, so when you had an email it would automatically send it immediately. Now we have two separate commands for this, we have deliver_later and deliver_now. And deliver_now will send immediately of course, and then deliver_later will also send immediately unless you've configured Active Job to use something other than the inline background worker. So if you install Sidekiq, you install Resque or Sucker Punch, you'll be able to send jobs to Active Job that will send the emails later. So by default it will send them immediately and you just have your application ready to go when you want to roll out background workers. So that's really cool, and we're going to be using deliver_later because there's no reason to use deliver_now unless you absolutely need to send it immediately.

After we create a forum post inside the forum post controller, we want to actually send that email notification. We can add a method immediately after save

forum_posts_controller.rb

def create
    @forum_post = @forum_thread.forum_posts.new forum_post_params
    @forum_post.user = current_user 

    if @forum_post.save 
        @forum_post.send_notifications!
        redirect_to forum_thread_path(@forum_thread, anchor: "forum_post_#{@forum_post.id}")
    else
        redirect_to @forum_thread, alert: "Unable to save your post"
    end
end

We'll make this send_notifications method on the forum post so it can be the one that contains the logic for this. So we'll have this send_notifications method and it needs to get all the unique users in this thread, and then it also needs to send an email to each of those users. So we're going to have to do two things, the first thing will be to gather up all the users inside the thread that the forum post is in, and then we're going to have to send an email to all of those.

So first thing's first, we can star building this one (gathering all the unique users), and that one (Sending emails) is pretty simple. So we have the forum thread, through our association, so we'll be able to do that, and then we'll be able to say, if we go to the forum thread, you can see that we have a belongs_to, but we can also add a has_many :users, through: :forum_posts. So this will be a list of all the users who have ever chatted inside of the forum thread, and this (user) will be the owner of the forum thread. We don't really need to interact with this, we're just saving it, just in case that we want to allow them later to be able to edit just the forum thread subject or something. So they're a little bit special because they're the person that created them, and we could give them a different name, belongs_to :owner, class_name: "User", or something like that so that it separates the two out, so I'm going to leave that for now. so we'll add this association, and then that gives us the ability to save forum_thread.users, and then we can take all of the unique users in that list, but can remove the current user from this forum post.

This(belongs_to :user) association, whoever made the post, we want to remove from there, because if you create a comment in a thread, then you don't need to get an email notification because you submitted it, so we're going to remove you from that array, and then we'll save this too, we'll just say users variable.

At this point, the models look a bit like this:

app/models/forum_thread.rb

class ForumThread < ActiveRecord::Base 
    belongs_to :user 
    has_many :forum_posts
    has_many :users, through: :forum_posts

    accepts_nested_attributes_for :forum_posts

    validates :subject, presence: true
    validates_associated :forum_posts 
end 

app/models/forum_post.rb

class ForumPost< ActiveRecord::Base 
    belongs_to :forum_thread 
    belongs_to :user 

    validates :body, presence: true 

    def send_notifications! 
        users = forum_thread.users.uniq - [user]
        #TODO Send an email to each of these name users
    end
end

This will allow us to take all the users in the thread, remove you, and then we can send the email out to. So to create our mailer, to do that next step is the app/mailers folder and inside here we can create a new mailer. I'm actually going to se the

rails g mailer NotificationMailer

Rails will generate this for you if you want, it also generates this preview (test/mailers/previews/notification_mailer_preview.rb) which we're going to take a look at just a little bit later.

So now we're going back to MacVim, and we can open up the mailers/notification_mailer.rb, and it's very simple, it's a class, it inherits from ActionMailer::Base, and then there are some defaults that you can set, such as the email it comes from.

app/mailers/notification_mailer.rb

class NotificationMailer < ActionMailer::Base 
    default from: "[email protected]"
end

So we can change "[email protected]" to "[email protected]", but then we actually need to create the methods for the actual emails that are sent.

This one will be a forum_post_notification, and we're going to take the user that we want to send it to, and the forum post. What we'll do is we'll go through here, and to send this out, we're going to say:

app/models/forum_post.rb

    def send_notifications! 
        users = forum_thread.users.uniq - [user]
        users.each do |user| 
            NotificationMailer.forum_post_notification(user, self).deliver_later 
        end
    end

This will take advantage of te background workers if you have them and if we don't have them it will send them immediately.

Inside the notification_mailer.rb, we can cache these variables, so that we have them, and we'll have the forum_post as a variable, we can also have the user, so that we can use these in the views, and this works very similarly to how controllers do. You need to have the instance variables, here the orange ones so that you can use them in your views.

Here we need to send a mail to the user, and we want to send it to their email address. We don't need to specify from inside here (because it's specified in the default), but we can set this subject as subject: "[GoRails] New post in #{forum_post.forum_thread.title}".

Now when there's a notification, you will see that there was a new post inside of that thread. Now we can open up the app/views/notification_mailer folder and see there's nothing in it, but we can create our templates in there now. And you might have noticed the similarities between controllers and mailers.

You've got the folder name that matches the name of the class, and rather than inheriting from application controller, you inherit from ActionMailer::Base, but each of these method names matches a template inside your views in the same folder, so they work very very similarly which is another benefit of Rails, because you know exactly where these files are going to be. So we edit the app/views/notification_mailer/forum_post_notification.erb Here we can add just any message

<h1>New Post in <%= @forum_post.forum_thread.subject %></h1>

<p><%= link_to "Reply to this comment", forum_thread_url(@forum_post.forum_thread, anchor: dom_id(@forum_post)) %></p>

Coming back to our forum thread, we can write a test post, create the forum post and we'll see that it posted correctly, and if we come back to our Rails logs, we should be able to scroll up and see that the ActionMailer sent the email out.

The forum notification sent this email, "From: [email protected]" to me, and then it had the content as well, and you can see notes around this, that are notes from ActiveJob when it got the job from the delivered_later method that we sent. This is pretty neat, and we can see that it's making all these comments and it was processed form the inline thing which is why I executed it immediately and all of this has happened through the global id gem that allows us to do something like we did here where we pass in the user and the forum posts and normally you would take and pass in the user_id and this would look up the user here instead.

The global id gem basically creates a way to serialize your objects because when this happens later, you can't pass in the object immediately because that user might not exist, the forum_post might not exist, you have to look them up later, so there's a lot more to think about with the background workers, but rails is providing these things to handle this for you.

You want to make sure you're on the latest version of 4.2.0 which is beta 2 so that this all works, because you might run into problems with global id otherwise, but it adds a lot of magic in there that you can pass in objects and not have to worry about background workers actually working a little bit differently and we want to save just ID numbers so we can look them up later.

Imagine you sent an email, or created a new post, as test user, and it sends us an email in the background. Imagine that Chris Oliver was changing his email address at the exact same time and that finished first. Sending this email would be going to the wrong email address, unless you sent over the ID where we could look up Chris Oliver's email and then find out that it just changed so we can send it to the right one, rather than sending one to the old address. So there are small subtle things that you have to think about when you're using background workers, but Rails tries to help you quite a bit.

Because Rails is so helpful, they’ve even gone as far as integrating email previews into Rails itself now. So you might have used a gem like a letter opener before. So when you send these emails out, you don't actually have to actually check your email constantly and wait for it to go through the server, and that's a lot of time consuming stuff and it's definitely not testable either. So what Rails has introduced recently it seems to be a very unknown feature is that in the test directory, that file that it created before is in the mailers/previews/notification_mailer_preview.rb allows us to build a method here that we can view in the Rails application. This is only in development, but if we do a def forum_post_notification just like we had before in our mailer. So if we get the app/mailer/notification_mailer.rb up again, let's just grab (user, forum_post), we don't need to accept parameters this time because we're going to define those, and we're actually going to call this method. So we're going to have NotificationMailer.forum_post_notification(User.first, ForumPost.first).deliver_now and this will actually send the email and then Rails will call this method when you visit this URL.

Let's grab the mailers here and open that up in the browser, and you can see that this view is build into Rails, it enumerates all of the mailer that you've set up previews for, so every one of those previews files, it will go through it, look at the mailer preview class name, it will show it here for you, and then all of the methods that you've created, so you can make them named accordingly, so I try to keep them matching, and then when you click on this you can get of course the view of just that mailer so you can drill down a little bit better, but when you click on the individual email, you'll be able to see that email that we saw in the terminal but in your browser. So we don't actually have any CSS in here but you can test the CSS out inside your browser and your Rails application and make sure that's just looking properly, so you don't have to send yourself an email or do any of that hard work, that's very time-consuming. You can put it here and then test this out with actual real users, you can go change user.first to anyone that you want, you can load specific users, you could generate random users, you can do whatever you want and preview those emails just right there in your browser.

This is probably one of the most underrated and most useful features when using emails and Rails, and I highly recommend that you practice using these email previews because it will save you a crazy amount of time.

We wouldn't actually be finished without integrating a background worker thing into our application, so I'm going to open up the Gemfile and I'm going to add the gem of Sucker Punch in here, and Sucker Punch is one of the many background jobs gems out there so there's Resque, sidekiq, sucker punch. This one is very lightweight and doesn't require running a separate Rails or Ruby application to run those jobs, so it's very very easy to set up, and this is all you have to do actually.

And then we can go into config/environments/production.rb and we'll add

config.active_job.queue_adapter = :sucker_punch

The reason that we're going to do this in production only is because we actually want them to run in the background in production, but in development we don't care, we don't need to be running them in the background, it might be good to run sucker punch in the background to make sure it's working, but aside from that, you don't really need it in development because you can send those emails immediately and test them out because it's probably what you're wanting to do.

In production when you have hundreds of users on your forum sending emails and posting and interacting with different things, you want to tell active_job to change the queue adapter from inline to sucker punch or sidekiq, and that way the emails using deliver_later will send them in the background and then this (send_notifications! in the forum_post model) will execute really quickly as opposed to when we do deliver_now or regular deliver. Thoser are very slow because it actually sends out a bunch of emails.

Imagine your forum thread users list is 100 users. Well if you have to send 100 users 100 emails, that is a ton of work, and when you click "Submit Forum Post", that is going to take a very long time, and you don't want your user to be waiting around and worrying, and then if they worried when they clicked the button to create their post, and it took five seconds, they might submit it again, and then you've got to set another set of a hundred emails, and then people get duplicates and all of that.

Rails has been spending a lot of time building the Active Job API and building things like deliver_later so that by default we have lots of functionality that's easy for us to use.

I hope this was helpful explaining a bunch of the different Active Job things and we're going to come and build an Active Job notification thing, just like deliver_later, but we'll build our own from scratch in the next episode.

Transcript written by Miguel

Discussion


Gravatar
Innokenty Longway on

Thanks for sharing this. Didn't know about it.


Gravatar
David Pell (90 XP) on

I have no audio from 10:05 to 11:05. Is that an issue with the screencast or just a particularly reflective section? :)

Gravatar
Chris Oliver (167,500 XP) on

Well shoot. That's definitely missing audio. I may not have the original audio to get that fixed unfortunately. Looks like I'm just explaining that you can set the anchor and your emails can link to and highlight the specific element on the page when you click the link.

Gravatar
Guest on

Ok, that makes sense. Thanks!

Another issue I'm running into is this:

[ActiveJob] [ActionMailer::DeliveryJob] [ea357c97-e2f4-42c4-aa5f-393469e9f6b5] Performing ActionMailer::DeliveryJob from SuckerPunch(mailers) with arguments: "AdminsMailer", "job_posted", "deliver_now", gid://myapp/Job/28

^^ my console output from ActiveJob. It's showing "deliver_now" as an argument even though my code looks like this: CustomerMailer.job_posted(@job).deliver_later

So my Jobs#create action is still waiting for these emails to be sent before redirecting.


Gravatar
Mark Radford (1,170 XP) on

We can no longer use `deliver_now` or `deliver_later` with preview as it will throw an Net::OpenTimeout error. "Methods must return a Mail::Message object which can be generated by calling the mailer method without the additional deliver." http://api.rubyonrails.org/...

Gravatar
Chris Oliver (167,500 XP) on

Very good to know! Thanks @Marklar

Gravatar
Josh Dance (30 XP) on

What should you do instead now?

Gravatar
Chris Oliver (167,500 XP) on

Just leave that part out. For example:

class NotifierPreview < ActionMailer::Preview
def welcome
Notifier.welcome(User.first)
end
end


Gravatar
Kohl Kohlbrenner on

@excid3:disqus in the send notifications method, could you sub current_user for user when subtracting from the unique list? Does [user] reflect :user in belongs_to :user relationship?

Gravatar
Chris Oliver (167,500 XP) on

Yes it does. It's basically creating an array with one item in it and that item is the User from the belongs_to relationship. You can't do "current_user" because it's a method that's only available in the controllers and views. In the model here, we only have access to the current ForumPost record and it's associations.

Gravatar
Kohl Kohlbrenner on

@excid3:disqus thanks! One more thing: is there an implicit 'self' before forum_thread.users.uniq - users? I would think so.

Gravatar
Chris Oliver (167,500 XP) on

The only real time you need to use "self" in the model methods is when you want to assign an attribute. Not using self will create local variables. If you're just working with the values and not changing them, you don't have to use "self".

Gravatar
Kohl Kohlbrenner on

right. I understand the reasoning as to why you didn't include it, but it is "there", correct?

Gravatar
Kohl Kohlbrenner on

i.e. self.forum_thread.users.uniq - users

Gravatar
Chris Oliver (167,500 XP) on

Yep, I believe so. Python makes it always explicit, but it can be a little confusing in Ruby since it isn't required to access but it is require to set attributes.

Gravatar
Kohl Kohlbrenner on

Also, are smtp settings already set up here? Do we need to use mandrill or some other client that you didn't show?

Gravatar
Chris Oliver (167,500 XP) on

Yeah they are. Check out the Mandrill episode to see how I set that up. https://gorails.com/episode...


Gravatar
CODElit on

Hi oliver I just installed sucker_punch so that my email should deliver later... but three days passed not delivered.. By deliver now.... it always delivered... Am I missing something???


Login or create an account to join the conversation.