Search code examples
ruby-on-railsemailunsubscribe

Rails Email with Unsubscribe link


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.


Solution

  • 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.