9

I am working on a Rails 4.2 app that has recurring weekly events that people register for. They will get a reminder email before each event (so weekly). I want a one click unsubscribe link on the email. This seems like a common task but I haven't found a good current solution. Some directions I have seen are to use MessageVerifier which was new to Rails 4.1 and doesn't require saving a token string to compare to in the database. What are the steps to accomplish this. I have a user model and an event model. Emails are sent to registered users who signed up for the recurring event.

Steve Carey
  • 2,216
  • 17
  • 22

2 Answers2

50

Here is the solution I came up with for an unsubscribe link for regular emails sent to subscribers. It uses MessageVerifier.

1. Generate a cryptographic hash of the user.id

In my Events controller I have a send_notice action that sends the email. I'll create a variable for the unsubscribe link and pass it to the mailer.

# app/controller/events_controller.rb
def send_notice
  ...
  @unsubscribe = Rails.application.message_verifier(:unsubscribe).generate(@user.id)
  EventMailer.send_notice(@event, @user, @unsubscribe).deliver_later
end

This takes the user id and generates a signed and encoded string variable @unsubscribe. Message verifier turns the user id into an indecipherable string of characters by mixing it with your secret_key_base (found in the file config/secrets.yml), and the name that you give the message (in this case I'm calling it :unsubscribe but it could be any name) as what they call a salt, and running it through an algorithm called SHA1. Make sure your secret_key_base stays secure. Use environmental variables to store the value in production. In the last line we pass the @unsubscribe variable to the mailer along with @event and @user variables.

2. Add the @unsubscribe hash variable to the mailer

# app/mailers/event_mailer.rb
def send_notice(event, user, unsubscribe)
  @event = event
  @user = user
  @unsubscribe = unsubscribe
  mail(to: user.email, subject: "Event Info")
end 

3. Put the unsubscribe link in the email

# app/views/events_mailer/send_notice.html.erb
...
<%= link_to "Unsubscribe", settings_unsubscribe_url(id: @unsubscribe) %>.

Notice the (id: @unsubscribe) argument added. This will append the encoded user id to the end of the URL beginning with a ? in what is known as a query param.

4. Add routes

Add a route that takes the user to an unsubscribe page. Then another route for when they submit the unsubscribe.

# config/routes.rb
get 'settings/unsubscribe'
patch 'settings/update'

5. Add a subscription field to the User model

I opted to add a subscription boolean (true/false) field to the user model but you can set it up many different ways. rails g migration AddSubscriptionToUsers subscription:boolean.

6. Controller - Decode the user_id from the URL

I opted to add a settings controller with an unsubscribe and an update action. Generate the controller rails g controller Settings unsubscribe. When the user clicks on the unsubscribe link in the email it will go to the url and append the encoded User ID to the URL after the question mark as a query param. Here's an example of what the link would look like: http://localhost:3000/settings/unsubscribe?id=BAhpEg%3D%3D--be9f8b9e64e13317bb0901d8725ce746b156b152. The unsubscribe action will use MessageVerifier to unsign and decode the id param to get the actual user id and use that to find the user and assign it to the @user variable.

# app/controllers/settings_controller.rb
def unsubscribe
  user = Rails.application.message_verifier(:unsubscribe).verify(params[:id])
  @user = User.find(user)
end

def update
  @user = User.find(params[:id])
  if @user.update(user_params)
    flash[:notice] = 'Subscription Cancelled' 
    redirect_to root_url
  else
    flash[:alert] = 'There was a problem'
    render :unsubscribe
  end
end

private
  def user_params
    params.require(:user).permit(:subscription)
  end

7. Views

Add the unsubscribe page that includes a form to cancel your subscription.

# app/views/settings/unsubscribe.html.erb
<h4>Unsubscribe from Mysite Emails</h4>
<p>By unsubscribing, you will no longer receive email...</p>
<%= form_for(@user, url: settings_update_path(id: @user.id)) do |f| %>
  <%= f.hidden_field(:subscription, value: false) %>
  <%= f.submit 'Unsubscribe' %>
  <%= link_to 'Cancel', root_url %>
<% end %>

There are a number of ways you can set this up. Here I use the form_for helper routed to the settings_update_path with an added argument of id: @user.id. This will append the user id to the URL when sending the form as a query param. The Update action will read it to find the user and update the Subscription field to false.

Steve Carey
  • 2,216
  • 17
  • 22
  • 2
    This answer is on point! Thank you Steve Carey. Your contribution will go down in history as one of my all time favorite ^__^ – SerKnight Aug 19 '16 at 18:20
  • @SteveCarey This solution throws `NoMethodError - undefined method 'id' for nil:NilClass` Is there anything am missing? How do I fix this? – Afolabi Olaoluwa Akinwumi Nov 15 '16 at 16:31
  • @SteveCarey is there any particular reason why you added a new controller for settings as opposed to adding these methods into your users_controller? BTW, thanks for the explanation, really really good! – Ben Smith Jan 16 '17 at 20:09
  • @ Ben Smith - I created a separate controller just to keep things a little more segregated. I didn't try it, but I'm sure you could just add these actions to the users_controller. – Steve Carey Jan 20 '17 at 20:49
  • Although the `unsubscribe` action is protected, the `update` in your answer seems to allow to unsubscribe anyone as long as you know their id. Shouldn't the verifier be passed along, instead? – Sunny Mar 08 '21 at 17:28
  • 1
    @Sunny, yes, I think you are right. To fix that you can either skip the update form step and just unsubscribe them in the unsubscribe action. Or instead of decoding the id verifier in unsubscribe action, just pass the encoded id from unsubscribe to the update action and decode it there. – Steve Carey Mar 09 '21 at 02:37
2

A really good option might be using Michael Hartl's Rails tutorial app. In chapter 12 he implements a system where users can follow other users microposts (basically tweets, twitter style). You would just need to set up the model validations (user and event models) and implement a Relationship model & controller. The relationship model and controller handle the whole following mechanism. Essentially the controller only has create and delete actions, and the create action would serve to create the relationship and the destroy to delete it. To handle the unsubscribe scenario you could simply have rails render the delete path in the ActionMailer template. The link above gives you step by step instructions, but if you get lost feel free to ask me questions.

Jarrel09
  • 185
  • 1
  • 15
  • Actually I'm one step ahead of you there. I did in fact do something similar to this. So the unsubscribe is set up as a destroy action in the Events controller. And it is activated by a button on the event page. So in the email I have a link back to the Event page and the user can follow the link, log in, then press the unsubscribe button on the event page. What I would like to add though, is the ability to do this simply by pressing the link without having to log in and press the button. – Steve Carey Dec 22 '15 at 07:40
  • I don't think having the destroy action in the events controller is what you want..It seems like that would destroy the actual event instead of the users "following relationship" with that event. Also, concerning the link in the email...you'll want to include some type of token system to authenticate the user and then insert the user's email and the hashed token as query string parameters into the url. Let me give you some examples: – Jarrel09 Dec 22 '15 at 09:27
  • Yes, I actually have it a bit more complicated than I mentioned. The structure is you have a recurring event and you have users. I created a join table called participations that has a many to many relationship with users and events. The user clicks a button on the recurring event's show page to become a participant of the recurring event. This creates a row on the Participations table with the user_id and event_id. The unsubscribe button destroys the participation row. – Steve Carey Dec 22 '15 at 21:55
  • ok, so then essentially the model and controller actions are functioning correctly? Then it sounds to me that you simply need the token function to create the token, then modify the delete action on the controller to authenticate the users email and token, and then to simply insert the user email and token through query string parameters into the email. Correct? If so I know how that can be done also – Jarrel09 Dec 22 '15 at 23:45
  • Yes the model and controller actions all work as intended. I do need to do something like you are suggesting. I found a gem called Mailkick that does this and I am experimenting with it. It does use the Rails 4.1 MessageVerifier and is not overly complicated. I generally avoid gems if I can just code it directly myself. – Steve Carey Dec 23 '15 at 02:13
  • Once again, I would suggest using the token method from chapter 10[link](https://www.railstutorial.org/book/account_activation_password_reset) of the tutorial. It not only explains how to implement the token creation and authentication, but also implementing it in ActionMailer. You could simply add a column to your participations table for the token digest, and then authenticate it and the users email on the destroy action in the participations controller. Also, can you please upvote my posts if I was helpful. I'm trying to build reputation points – Jarrel09 Dec 23 '15 at 03:46