Published by Dan on Feb 21, 2015

Mailing Lists

Filed under Web Services, Features


Even though mailing lists have been around since ARPANET, they remain a viable way for groups to communicate. Modern web applications contain dynamic user groups and should leverage email to facilitate communication within them.

Many web applications already send email notifications, and the best applications support replying to that email:

  • Github allows developers to discuss issues and pull-requests
  • Craigslist allows buyers and sellers to communicate anonymously
  • Tender (not Tinder) allows support to address customer issues
  • Basecamp allows all sorts of project discussion

If your web application sends emails, you should handle the reply button and often the best user-experience encourages its use.

The Implementation

Only the largest companies run email servers, while others send and receive emails using third-party services. We'll use ActionMailer to send emails and the griddler gem to receive emails through MailChimp's Mandrill service.

Model Design

Your application probably already separates users into groups, whether by admins, accounts, trial users, or long-term customers, but for this tutorial we'll use a more general design.

Users have memberships to groups. Group members generate discussions by sending messages. Five models total, here's the database schema:

# db/migrations/20150221052903_create_group_discussions.rb

create_table :users do |t|
  t.string :name
  t.string :email

create_table :groups do |t|
  t.string   :name
  t.string   :email
  t.datetime :digest_last_sent_at

create_table :memberships do |t|
  t.integer :user_id
  t.integer :group_id
  t.boolean :receives_every_message, default: false
  t.boolean :receives_digest,        default: false
  t.string  :token,                  null: false

create_table :discussions do |t|
  t.integer :group_id
  t.string  :email
  t.string  :subject

create_table :messages do |t|
  t.integer :discussion_id
  t.integer :from_id
  t.text    :content


A user has a name and an email address that can send messages to groups they have a membership in.

class User < ActiveRecord::Base

  has_many :memberships
  has_many :groups,        through: :memberships
  has_many :sent_messages, foreign_key: 'from_id'

  validates :name,  presence: true, format: {with: %r(^[\w\ ]+$)}
  validates :email, presence: true, uniqueness: true



Groups have an email address and users that are allowed to create discussions by emailing it.

class Group < ActiveRecord::Base

  has_many :memberships, dependent: :destroy
  has_many :users,       through: :memberships
  has_many :discussions, dependent: :destroy
  has_many :messages,    through: :discussions

  validates :name,  presence: true, uniqueness: true
  validates :email, presence: true, uniqueness: true



Memberships give users access to groups and also indicate what emails the user would like to receive from the group.

class Membership < ActiveRecord::Base

  belongs_to :user
  belongs_to :group

  validates :user_id,  presence: true, uniqueness: {scope: :group_id, message: 'is already a member of this group'}
  validates :group_id, presence: true
  validates :token,    presence: true

  before_validation :generate_token, on: :create


  # tokens uniquely identify a membership for
  # the purposes of unsubscribing through an email's link
  def generate_token
    loop do
      self.token = SecureRandom.hex(64)
      break if Membership.where(token: token).empty?



A discussion is a collection of messages within a group. Users add messages to the discussion by emailing it.

class Discussion < ActiveRecord::Base

  belongs_to :group

  has_many :messages, dependent: :destroy

  validates :email,   presence: true
  validates :subject, presence: true

  before_validation :generate_unique_email, on: :create


  # if the group's email is,
  # its discussion emails are
  def generate_unique_email
    loop do ='@', "-#{SecureRandom.hex(32)}@")
      break if Discussion.where(email: email).empty?
    end if group



A message is text sent by a user inside a group discussion.

class Message < ActiveRecord::Base

  belongs_to :discussion
  belongs_to :from, class_name: 'User'

  validates :from_id, presence: true
  validates :content, presence: true


Griddler Setup

The griddler gem smooths the process of receiving emails from third-party services such as Mandrill, SendGrid, Mailgun, and Postmark. I prefer Mandrill's simple but powerful interface, and MailChimp being my neighbor in Atlanta doesn't hurt.

First, add griddler and griddler's mandrill adapter to your Gemfile and run bundle install

# Gemfile

gem 'griddler'
gem 'griddler-mandrill'

Next, add the routes Mandrill will use to communicate to your app through griddler.

# config/routes.rb

# verifies during initial setup
get '/mandrill', to: proc { [200, {}, ["OK"]] }

# indicates a single received email
post '/mandrill', to: 'griddler/emails#create'

Finally, configure griddler to send received emails to the background job queue.

# config/initializers/griddler.rb

class Griddler::EmailProcessor
  def initialize(email)
    @email = email

  def process
      'from'    => @email.from,
      'to'      =>,
      'subject' => @email.subject,
      'body'    => @email.raw_body,

Griddler.configure do |config|
  config.email_service = :mandrill
  config.processor_class = Griddler::EmailProcessor

For more information on griddler, please refer to thoughtbot's blog post and the github repository.

Background Jobs

Our mailing list logic lives in background jobs and is actually rather simple:

  • Ignore emails if the sender doesn't belong to the group
  • If addressed to a group, create a new discussion and the initial message
  • If addressed to a discussion, create a new message
  • If neither a group or discussion is found, ignore it
  • Forward created messages to others in the group
class ReceiveEmailJob < ActiveJob::Base
  queue_as :default

  def perform(email)
    @from = User.where(email: email['from']['email']).first
    return unless @from # unknown sender

    @subject = email['subject']
    @body = email['body']

    email['to'].each do |to|
      try_group(to['email']) || try_discussion(to['email'])


  def try_group(email)
    group = Group.where(email: email).first
    return unless allow_messages_to?(group)

    discussion = @subject)
    message = @from, content: @body)

    forward(message) if

  def try_discussion(email)
    discussion = Discussion.where(email: email).first
    group = if discussion
    return unless allow_messages_to?(group)

    message = @from, content: @body)

    forward(message) if

  def allow_messages_to?(group)
    group && group.memberships.where(user_id: @from).any?

  def forward(message)

class ForwardMessageJob < ActiveJob::Base
  queue_as :default

  def perform(message)
    @message = message

    memberships.each do |membership|
      # spawn a new job for each email in case any fail to send
      GroupsMailer.new_message(membership, message).deliver_later


  def memberships
      where(receives_every_message: true).
      where('user_id != ?', @message.from)


Outgoing emails follow the Action Mailer Basics, though notice the "from name" is the sender but the "from email" is the discussion, ensuring replies are handled properly by our application.

# app/mailers/groups_mailer.rb
class GroupsMailer < ApplicationMailer

  def new_message(membership, message)
    @membership = member
    @message = message
    @discussion = @message.discussion
    @group =

      from: %("#{}" <#{}>),
      subject: "[#{}] #{@discussion.subject}"

<!-- app/views/groups_mailer/new_message.html.erb -->
<%= simple_format @message.content %> 

Mandrill Setup

Your top-level domain is probably already using an email service like gmail, so it's best to establish a subdomain like for sending and receiving emails programmatically. Follow Mandrill's documentation to setup your account:

  1. Creating a Mandrill Account
  2. Setting up a Sending Domain
  3. Adding an Inbound Domain
  4. Adding a New Route to your Inbound Domain

The end result should be:

  • Sending domain marked DKIM valid and SPF valid
  • Inbound domain marked MX valid with a verified route * with a webhook URL of

Your production environment is now setup to run mailing lists from *

Rounding the Edges

A few more features before we have a proper, user-friendly mailing list:

Future Considerations

Now we've wrapped up the basic mailing list functionality, though there's plenty more to think about:

  • How are groups managed?
  • How do users enable the daily digest?
  • Should we support attachments?
  • Can users be deleted?
  • Can non-members view an archive?
  • Can users create messages inside the archive?
  • How can we ensure only Mandrill can post received emails?

All these questions can be approached using standard Ruby on Rails MVC, and none are especially difficult with the existing design.


Here's what we made:

  • Groups of users can have discussions via email
  • Daily digests are sent to members that don't want to see every message individually
  • Unsubscribe links allow members to stop receiving a group's emails
  • A web archive exposes a group's complete discussion history

Essentially we have the most important parts of Google Groups, but the real possibilities come to light when you think beyond generic groups and messages. You can send and receive emails in your existing application:

  • How are your users grouped?
  • How can email help these groups communicate?
  • What could have an email address?
  • What would be convenient for your users to post from their inbox?

Work the reply button into your application's workflow. is no longer a viable option. At the very least use and forward it to an intern.


© 2012-2018 Dan Cunning. All rights reserved.