mailkick: unsubscribe links and opt-out fetching for Rails
Email subscriptions for Rails
At a glance
- What is it?
- mailkick is a small Rails gem with two jobs. It adds one-click unsubscribe links and RFC 8058 headers to the mail you send, and it fetches bounces, spam complaints and opt-outs from whichever email service you use, so that a subscriber who unsubscribed at the provider is unsubscribed in your database too.
- Who is it for?
- mailkick suits a Rails application that sends mail to a list and needs to honour unsubscribes and provider-side suppressions, and its scope is deliberately small, which makes it easy to read and easy to replace. It is a poor fit if you need analytics on the sending side, since the readme points at a separate companion gem for that, and a poor fit if you are on a version of Rails that has already changed the key generator digest behaviour.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 76 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two jobs, and the second one is the interesting one
The gem is described in two bullets, and they are not the same kind of work.
The first is to add one-click unsubscribe links and headers to your emails. The second is to fetch bounces and spam reports from your email service.
The first job is table stakes. Any mailing system needs an unsubscribe link, and doing it well means generating a signed URL, rendering a page, handling the resubscribe case, and setting the header that mail clients look for. The second job is where most applications get it wrong, because a subscriber can opt out at the email provider rather than in your application, and if you do not reconcile the two, your list keeps sending to people who have already asked to stop.
That reconciliation is what the second bullet is for. The gem talks to the provider, gets the list of suppressed destinations, and hands them to a block of your code that removes the matching subscriptions. The logic of what to do with an opt-out is yours, because the policy question, is this a bounce or a complaint, and should a complaint remove all subscriptions, is specific to your application.
The gem also leaves a clear boundary for the thing it does not do. There is a pointer at the top of the readme to Ahoy Email, by the same author, for analytics, which tells you the design intent: this is the subscription and suppression library, and tracking opens and clicks is somebody else's problem.
The licence is MIT, the language is Ruby, and the readme is short enough to read in full, which for a gem of this size is a good sign about how much of the behaviour is in the code and how much is configuration.
Installation is a gem and a generator
The installation follows the standard Rails pattern, with one step that is specific to this gem.
gem "mailkick"Then the generator, which creates the table that stores subscriptions:
bundle install
rails generate mailkick:install
rails db:migrateThe order matters in the ordinary way, the generator writes the migration and the migration creates the table, and the readme notes that the generator creates a table to store subscriptions.
That table is the whole data model. A subscription is a user, a list name and a timestamp, and the timestamp is what makes the opt-out handling work, because when you process a suppression from the provider you destroy only the subscriptions that were created before the opt-out happened. A person who re-subscribed after complaining stays subscribed.
The model class is available directly if you need it, and the readme mentions a way to prefix the method names, which matters when the gem is installed into an application that already has a subscribe method of its own:
class User < ApplicationRecord
has_subscriptions prefix: true
endWith the prefix, the calls become namespaced and cannot collide with anything in your own codebase, which is a small option that saves an afternoon when it is needed.
Four methods on your user model
The model API is four calls, and they read the way you would guess.
class User < ApplicationRecord
has_subscriptions
endThe macro adds the subscription methods to the model. Then there is a subscribe call, an unsubscribe call, a predicate asking whether the user is subscribed to a list, and a class-level call that returns the subscribers for a list, which the readme says is the one to use for sending emails.
user.subscribe("sales")
user.unsubscribe("sales")
user.subscribed?("sales")
User.subscribed("sales")The list name is a plain string, which is the design decision worth noticing. It means a list is not a model, there is no list table, no list administration and no referential integrity. Adding a new kind of newsletter is a string in your code rather than a row in a table, and a subscriber to that list is a row in the same subscriptions table with a different name.
That is a deliberate simplification and it has a consequence. If your application needs to know which lists exist, or to have list level settings, or to report on list growth, you are keeping that information yourself, because the gem will not hold it for you. For a small application with a fixed set of lists, that trade is a good one. For a product where marketing manages lists in a database, it is the wrong shape and you should look at something heavier.
Unsubscribe links, headers and the scanner problem
The link itself is a helper, with a version for HTML mail and one for text mail.
<%= link_to "Unsubscribe", mailkick_unsubscribe_url(@user, "sales") %>Unsubscribe: <%= mailkick_unsubscribe_url(@user, "sales") %>When a user follows it, they are taken to a mobile-friendly page and given the option to resubscribe, which is the correct behaviour: an unsubscribe link that also offers a way back is much less likely to be clicked by mistake, and the resubscribe path is what you want when a user hits the link on the wrong device.
The view can be replaced by running a views generator, which copies the view into your application so you can edit it.
Two settings control the behaviour, and both live in an initializer file.
The first is a confirmation step, enabled by setting a flag to true, and the readme explains why: to prevent issues with scanners. Automated link checkers follow every URL in a newsletter, including unsubscribe links, and a mail system that unsubscribes a subscriber because a scanner followed the link is a mail system nobody trusts. A confirmation step means the request is recorded but nothing changes until a human confirms it.
The second setting disables the one-click unsubscribe headers, which are added automatically to any email that calls the unsubscribe url helper. The headers are the RFC 8058 mechanism, the List-Unsubscribe header and the one-click List-Unsubscribe-Post, and they let a mail client offer an unsubscribe button that takes effect without the user visiting a page at all. Since they are added automatically, a project that does not want them has to turn them off explicitly.
Fetching opt-outs from six providers
The opt-out handling is a two part contract. You provide a method that does the work, and the gem calls it with the opt-outs it fetched.
Mailkick.process_opt_outs_method = lambda do |opt_outs|
emails = opt_outs.map { |v| v[:email] }
subscribers = User.includes(:mailkick_subscriptions).where(email: emails).index_by(&:email)The example does the work in the right order. It collects the emails, loads the matching subscribers with their subscriptions included in a single query rather than one per subscriber, indexes them by email, and then, for each opt-out, destroys only the subscriptions older than the opt-out time.
Mailkick.fetch_opt_outsis the call that runs the fetch. The point of putting your policy in a lambda is that the gem does not have to guess: a bounce is not a complaint, and a complaint might warrant unsubscribing someone from everything rather than from the list that was reported.
Six services are supported, and each needs a gem and a key. Amazon SES needs the AWS SDK for the version 2 API and the permission to list suppressed destinations, with a note about accounts that started using SES before November 25, 2019 having to enable the account level suppression list feature manually. Mailchimp needs the Gibbon gem and both an API key and a list identifier. Mailgun, Mandrill and Postmark each need their own gem and a single API key, and SendGrid needs its gem and an API key with only the suppressions permission, which is a nicely minimal scope.
For anything more specific, the services can be set by hand, and the readme gives the pattern: an array of service objects, each constructed with the credentials it needs.
The 3.0 upgrade has a hash digest trap in it
There is an upgrade section, and it contains a genuine trap.
Versions 1.1.0 through 2.0.0 cause applications to use SHA1 for the key generator hash digest class, and the reason given is a behaviour change in Rails itself, tracked as an issue in the Rails repository. To avoid breakage when upgrading, you are told to add a line to your application configuration file:
config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA1The reason this matters is that the digest is what signs the unsubscribe URLs. If the digest changes, every URL you have already sent out stops validating, so a user clicking an unsubscribe link in a newsletter from last month gets an error instead of a page. Pinning the digest keeps the old URLs working, and the gem is telling you to do it in the application rather than inside the gem so that the behaviour is explicit at the application level.
The second part of the upgrade is simpler. One-click unsubscribe headers are enabled by default in 3.0, and to turn them off you create an initializer with the headers flag set to false. That default is a change in behaviour rather than a bug, and it is the kind of change that is invisible until a mail client starts showing an unsubscribe button in a place you did not expect.
The changelog is linked for anything else, and the upgrading section is short enough to read before you bump the version rather than after something breaks.
Repository shape and what the gem does not promise
The repository is a conventional gem layout, with a lib directory, an app directory for the views and the mailer, a config directory for the templates, a test directory and a Rakefile, plus a gemspec, a Gemfile and a changelog.
One directory is worth mentioning: there is a gemfiles directory, which is the conventional way to test a gem against several versions of its dependencies. Each file there pins a different set of dependency versions, and the continuous integration matrix runs the test suite against each of them. For a gem that depends on Rails and on the API clients of six email providers, that is the difference between knowing that version 3.0 works with the current Rails and finding out in a customer's production environment.
On maintenance, the repository is not archived and the last push was on 2026-08-08, so the work is still happening, though there are no GitHub releases, so the version to install comes from the gem registry rather than from a tag.
The honest summary is that this is a small, well-scoped, well-documented gem that does two things, one of which most applications do badly, and that it will not grow into a mailing list manager. If your requirement is a full campaign management system with templates, segments and reporting, this is the wrong tool and the readme even points you at a companion project for the analytics half. If your requirement is that a subscriber who unsubscribes at the provider stops receiving your mail, and that the unsubscribe link in a six month old newsletter still works, this is exactly the right size of tool.
Editorial conclusion
mailkick suits a Rails application that sends mail to a list and needs to honour unsubscribes and provider-side suppressions, and its scope is deliberately small, which makes it easy to read and easy to replace. It is a poor fit if you need analytics on the sending side, since the readme points at a separate companion gem for that, and a poor fit if you are on a version of Rails that has already changed the key generator digest behaviour. Before you adopt it, read the upgrade section for 3.0, because the SHA1 digest setting affects every signed unsubscribe URL your application has already sent, and set the confirmation step if scanners are hitting your unsubscribe links.
Frequently asked questions
How do I install mailkick in a Rails app?
Add the gem to the Gemfile, run bundle install, then run the install generator and the migration, with rails generate mailkick:install followed by rails db:migrate. The generator creates the table that stores subscriptions.
How does mailkick handle opt-outs reported by my email provider?
You set a process_opt_outs_method lambda that receives the fetched opt-outs, and each one carries an email and a time. The example loads the matching subscribers in one query and destroys only the subscriptions created before that time, so a subscriber who re-subscribed later stays subscribed. Mailkick.fetch_opt_outs runs the fetch.
Which email services does mailkick support?
Amazon SES, Mailchimp, Mailgun, Mandrill, Postmark and SendGrid. Each needs its own gem and API key, and the permissions are narrow, with SES requiring the list suppressed destinations permission and SendGrid requiring only the suppressions permission. Services can also be constructed by hand for more control.
What should I do before upgrading to mailkick 3.0?
Add config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA1 to config/application.rb, because earlier versions caused applications to use SHA1 and changing the digest would invalidate the unsubscribe URLs you have already sent. Also note that one-click unsubscribe headers are enabled by default in 3.0 and can be turned off in an initializer.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ankane-mailkick)Was this useful?