Implementing List-Unsubscribe in Rails 6

19 July 2020

A while ago, Marc Kohlbrugge asked me to implement an interesting feature into startup.jobs that I had not previously considered building in my apps. Because it provides a great benefit to the user, I have written this blog post for people who want to implement the List-Unsubscribe header with Rails.

First, I’ll explain about this feature, which you may have seen in emails you have received. On Apple Mail.app on iOS, you’ll often see this in the header of an email:

Example of how it looks in iOS

By clicking on the “Unsubscribe” button you get automatically unsubscribed from this email list. You might wonder how this works. When you look at the raw email you’ll see this:

header

Here you can see that the sender of this email included a List-Unsubscribe header to the email. This way, email clients can provide you a way to opt-out of future emails. When you click the button in your client, the email client sends an email on your behalf to the specified email address.

In this blog post, I’ll explain how to implement this using ActionMailbox.

Add a secure token to the user table

When a person unsubscribes, it’s important to know who that user is. The easiest and most secure way to do this is by adding a new field to the database, which I call the unsubscribe_token. You could say: Why not use user.id? However, that would allow users to unsubscribe other users by providing the other person’s id.

The migration to add the token:

class AddUnsubscribeTokenToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :unsubscribe_token, :string

    add_index :users, :unsubscribe_token
  end
end

Then also add it to the User model:

class User < ApplicationRecord
  has_secure_token :unsubscribe_token
end

Route and process incoming email

Write the route in the file called app/mailboxes/application_mailbox.rb. This handles every incoming email that starts with unsubscribe-. You can also choose something else, but be sure that you supply the right address in the header.

class ApplicationMailbox < ActionMailbox::Base
  routing(/unsubscribe-/i => :unsubscribers)
end

Then create the Mailbox (you could compare this to a regular Rails controller). For another example, see the documentation.

class UnsubscribersMailbox < ApplicationMailbox
  before_processing :set_user

  def process
    @user.update_column(:send_saved_search_emails, false)
  end

  def set_user
    recipient = inbound_email.mail.to.first
    unsubscribe_token = recipient.scan(/unsubscribe-(.+?)@/i)

    inbound_email.bounced! if unsubscribe_token.blank? || (@user = User.find_by(unsubscribe_token: unsubscribe_token)).blank?
  end
end

Add header to the email

Then final step is to add the header to the email. In a mailer, you can add headers to the mail by listing them in the call to mail.

class SavedSearchEmailMailer < ApplicationMailer
  def new_posts(user, new_posts)
    @user = user

    mail to: @user.email,
         subject: title,
         "List-Unsubscribe": "<mailto:unsubscribe-#{@user.unsubscribe_token}@startup.jobs>"
  end

Configure your incoming email to be received by Rails

This is best explained in the Rails documentation for ActionMailbox.