E-mails in Ruby on Rails

by Anton TrushkevichJuly 29, 2014
This blog post explores different scenarios of working with e-mails in Ruby on Rails: sending and receiving an e-mail, as well as using address tagging.

Sending an e-mail

In Ruby on Rails, an e-mail is sent with the use of the ActionMailer::Base class. You can check the documentation to get more details.

 
Configuration

It appears to be convenient to configure ActionMailer in different ways for different environments.

 
Development

A great tool for this environment is the Letter Opener gem. It intercepts all outgoing e-mails and opens each e-mail in a separate tab of your default browser instead of sending it right away. This allows you not to worry about sending unwanted e-mails by accident to your real users’ mailboxes, as well as not to bother your customers and testers during development or debugging some mailer. Below is an example of ActionMailer config for the development environment.

# config/environments/development.rb

config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.default_url_options = { host: 'localhost:3000', protocol: 'http' }

Another option is to use the MailCatcher gem, which is also great. However, Letter Opener is just simpler, so I prefer using it.

 
Staging

It’s rather common to have a three-environment infrastructure: development, staging, and production. The staging environment is usually used by testers and/or customers for testing features to work properly before releasing them to production. In this case, it’s important for a tester to be able to verify that some e-mail is sent successfully and implemented correctly. At the same time, e-mails should not be sent to real users. In this case, Letter Opener is not an option. MailCatcher would be suitable here, but again, there is a simpler and more convenient option—Mailtrap. You can register a basic account (which is pretty sufficient in most cases) for free. With this approach, e-mail delivery is actually performed, but to the Mailtrap inbox instead of end users’. In addition, as expected, Mailtrap provides a web interface to manage sent e-mails. Below is an example of ActionMailer config for the staging environment.

# config/environments/staging.rb

config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = false
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
user_name: 'examplecom-staging-g8257th95725e9k1',
password: 'mypassword',
address: 'mailtrap.io',
port: '2525',
authentication: :plain,
}
config.action_mailer.default_url_options = { host: 'staging.example.com', protocol: 'http' }

 
Production

In this environment, everything is pretty obvious, as you should have ActionMailer to be configured for e-mail delivery to real users. The ActionMailer config example for the production environment looks like this.

# config/environments/development.rb

config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = false
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'mail.example.com',
domain: 'example.com',
user_name: 'myusername',
password: 'mypassword',
authentication: :plain,
enable_starttls_auto: true,
port: 587,
}
config.action_mailer.default_url_options = { host: 'example.com', protocol: 'http' }

By the way, if anyone uses Gmail for sending an e-mail, below is an example of Simple Mail Transfer Protocol settings for it.

config.action_mailer.smtp_settings = {
address: 'smtp.gmail.com',
domain: 'example.com',
user_name: 'myusername@gmail.com',
password: 'mypassword',
authentication: :plain,
enable_starttls_auto: true,
port: 587,
}

 
Mailers

Instead of inheriting your mailers directly from ActionMailer::Base, it is more convenient to create a parent ApplicationMailer mailer class where you can configure default mailer properties, such as layout and from. Later, you can create your mailers inherited from it.

# app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base

layout 'application_mailer'
default from: 'postoffice@example.com'

end

Having a layout provides you with obvious advantages of DRYing your e-mail views (for example, you can easily add a shared header and footer).

/ app/views/layouts/application_mailer.html.haml

%html
%head
%meta{'http-equiv' => "Content-Type", content: "text/html; charset=UTF-8"}
%body
= render 'shared/email_header'
%hr
= yield
%hr
= render 'shared/email_footer'

 
A custom mailer

So, let’s suppose that your application provides users with a capability to send in-site messages to each other with an option to send an e-mail to a recipient user, as well (via a checkbox, etc.). For the purpose, a possible simplified create action responding to JavaScript format could look something like this.

# app/controllers/messages_controller.rb

class MessagesController < ApplicationController

def create
@recipient = User.find(params[:recipient_id])
@message = current_user.messages.build(message_params)
if @message.save
MessageMailer.new_message(@message, recipient).deliver
else
# some error processing
end
end

end

By the way, for performance and usability reasons, it is better to send e-mails asynchronously, for example, using Sidekiq gem. In this case, your code would look like this.

MessageMailer.delay.new_message(@message, recipient)

Ok, let’s look at our MessageMailer.

# app/mailers/message_mailer.rb

class MessageMailer < ApplicationMailer

def new_message(message, recipient)
@message = message
mail({
subject: message.subject,
to: recipient.email,
})
end

end

And we should have a corresponding view.

/ app/views/message_mailer/new_message.html.haml

%h2= @message.subject
%p= simple_format @message.content

That is all. A basic e-mail should now be sent after invoking the create action of MessagesController.

 
A pretty From field

Let’s modify the From field of the e-mail a bit in order to clearly see who has sent you a message when you receive such an e-mail. Let’s also not show an e-mail address of the actual user an e-mail address (let’s assume it is for private reasons), but instead we’ll use the e-mail address that we set as a default one in the ApplicationMailer class.

mail({
from: "#{message.author.full_name} <#{default_params[:from]}>",
subject: message.subject,
to: recipient.email,
})

Assuming a message was sent by John Doe. Gmail will show the email was sent as from “John Doe.” Most e-mail clients, however, will show it as from “John Doe <postoffice@example.com>.” I think it looks much better than just from “postoffice@example.com.” Besides, it allows you to search for e-mails based on the actual sender.

Attachments

Suppose that we want to send some file attachments along with an e-mail. As file uploads are out of the scope of this blog post, let’s just assume that we have the following models.

class Message < ActiveRecord::Base
MAX_ATTACHMENTS_TOTAL_SIZE = 20 * 1024 * 1024
has_many :content_files
belongs_to :author, class_name: 'User', foreign_key: 'created_by'
end
class ContentFile < ActiveRecord::Base
mount_uploader :attachment, ContentFileUploader
belongs_to :message
end

In the example above, the ContentFile model has a mounted :attachment—the CarrierWave uploader (yep, I prefer to use CarrierWave for file uploads).

You should also keep in mind that you can’t send an arbitrary amount of data in attachments, as it’s most likely that e-mail size will be limited at the destination mail server. For example, the current Gmail total limit of e-mail size limit (including body and attachments) is equal to 25 MB. So, we should process attachments somehow taking into account their size. There are plenty of options that you could implement, including blocking messages with attachment size overlimit from being sent at all. To my mind, though, a better solution is to send just as much data as possible, and if there are attachments that were not sent then explicitly, tell about it to the recipient user and invite him to see an original message at your website. Code responsible for this could be as follows.

class MessageMailer < ApplicationMailer

def new_message(message, recipient)
@message = message

total_attachments_size = 0
message.content_files.each do |content_file|
next if total_attachments_size + content_file.filesize >= Message::MAX_ATTACHMENTS_TOTAL_SIZE
attachments[content_file.title] = File.read(content_file.attachment.file.path)
total_attachments_size += content_file.filesize
end

mail({
from: "#{message.author.full_name} <#{default_params[:from]}>",
subject: message.subject,
to: recipient.email,
})
end

end

The algorithm above, of course, is not the most perfect one and can be improved in many ways, depending on your needs. This implementation tries to create attachments based on first uploaded files first, assuming that they are the most important. Then it tries to add as many files as possible up to the provided limit, skipping large files causing overlimit.

As for content_file.filesize, I usually store filesize in a database for faster access to it and to avoid additional disk operations.

Great! Now, we can send e-mails with attachments. Next, let’s see how to receive an e-mail in Ruby on Rails.

 

Receiving an e-mail

Without any doubts, it would be very useful to not only send e-mails but also receive them and process in the context of your Ruby on Rails application. The most common solution for this task is to use the Mailman gem. Setting up Mailman is pretty simple. Let’s see how to configure it to fetch an e-mail from a Gmail account. First, add it to your Gemfile

gem 'mailman', require: false

and run bundle install. Next, let’s create a file that we will use to run Mailman as a background process.

# script/mailman_daemon

#!/usr/bin/env ruby
require 'daemons'
Daemons.run('script/mailman_server')

We will be able to run it almost as a standard UNIX daemon.

bundle exec script/mailman_daemon start|stop|restart|status

Next, let’s create the actual Mailman server script. Below is an example of how it could look like.

#!/usr/bin/env ruby
require "mailman"

Mailman.config.logger = Logger.new(File.expand_path("../../log/mailman.log", __FILE__))
Mailman.config.poll_interval = 60
Mailman.config.pop3 = {
server: 'pop.gmail.com', port: 995, ssl: true,
username: 'myemail@gmail.com',
password: 'mypassword',
}

Mailman::Application.run do
to '%folder%@example.com' do
# at this point we have "message" and "params" methods available
# so you can check fetched message and params. Everything before the "@" character
# will be available as params[:folder]
Mailman.logger.info message.inspect
Mailman.logger.info params.inspect
end

default do
# this is a catch-all route
end
end

We configured Mailman to get an e-mail from a Gmail account via the Post Office Protocol once in a minute. Within the Mailman::Application.run block, we define rules to determine how to process an e-mail in a way very similar to the Ruby on Rails router approach—you define routes one by one, and the first suitable route’s block will be executed. Pretty simple, isn’t it? Great! Now, you can both send and receive e-mail in Ruby on Rails.

 

Address tags

Another very important and useful technique anyone should be familiar with is address tagging, which is sometimes referenced as sub-addressing (e.g., in RFC 5233). The point of this technique is that you can provide some additional info when you send an e-mail right in an email address after a certain separator (usually the + character) this way: me+tag1-tag2@example.com. All e-mails sent to this address will be actually delivered to the me@example.com address.

One of the most well-known use cases of this technique is to determine web sites whose database was stolen (or sold, who knows). So, imagine that you have an e-mail address johndoe@example.com, and you want to register at some web site www.awesomesite.com. During registration, provide your e-mail as johndoe+www.awesomesite.com@example.com. If after some period of time you start receiving magic pills advertising e-mails with the To header field equal to johndoe+www.awesomesite.com@example.com, then it’s pretty obvious where those spammers got your e-mail. Sometimes web sites can have e-mail validation rules that will reject your e-mail address containing + or some other characters.

You should also keep in mind that this technique can’t be used in some cases, because not all mail servers support it or have it enabled by default (e.g., Gmail supports it). So, if you’re setting up your own mail server, then you should better check the documentation to be sure.

 

Ruby on Rails, Mailman, and address tagging

A very interesting functionality can be achieved by combining the Ruby on Rails, Mailman, and address tagging techniques. Imagine you have a website where users can send messages to each other, and you also automatically send a copy of a message by e-mail (or users can manually choose to send a copy to email)—a pretty standard feature. It would be great if a recipient user could reply to your e-mail right in his/her e-mail client. Let’s see how we can do it. Further, I provide a pseudo-code that can lack some details, but is sufficient to get the idea.

Let’s suppose we have following models.

class User < ActiveRecord::Base
end
class MessageDelivery < ActiveRecord::Base
belongs_to :recipient, class_name: "User", foreign_key: "recipient_id"
belongs_to :message
end
class Message < ActiveRecord::Base
belongs_to :author, class_name: "User", foreign_key: "author_id"
has_many :message_deliveries
has_many :recipients, through: :message_deliveries
end

The MessagesController controller with the create action responsible for sending messages looks like this.

class MessagesController < ApplicationController

def create
@message = Message.new(message_params)
if @message.save
@message.recipients.each do |recipient|
# as I mentioned previously it's strongly recommended to process email sending in background
MessageMailer.delay.email_copy(@message, recipient)
end
else
# some error processing
end
end

end

Our mailer class, MessageMailer, could look like this.

class ApplicationMailer < ActionMailer::Base
layout 'application_mailer'
default from: 'postoffice@example.com'
end
class MessageMailer < ApplicationMailer

def email_copy(message, recipient, options = {})
@message = message
mail({
from: "#{message.author.full_name} <#{default_params[:from]}>",
subject: "New message",
to: recipient.email,
reply_to: default_params[:from].gsub('@', "+f-#{recipient.uuid}-t-#{@message.author.uuid}-m-#{@message.uuid}@"),
})
end

end

In the example above, I set the Reply-to field to contain our default From e-mail address with addition of some useful information, using address tagging. What it gives is that a user will still receive e-mails from the postoffice@example.com address. When a user hits “Reply” in his email client, however, than the To e-mail address will be equal to the one we passed in the Reply-to field. Of course, we make an assumption that a user will not change it (I think that in most cases he/she will not indeed.)

To my mind, it’s better to send some hashed values rather than just plain ids, as the system will be more resistant to fraud actions in this case. It’s also better to use one-time hashes and expire them after a message is received.

The Mailman route for catching such e-mails can look like this.

Mailman::Application.run do
to 'f-%from_uuid%-t-%to_uuid%-m-%message_uuid%@' do
# here you can load all records that you need
from_user = User.find_by_uuid(params[:from_uuid])
to_user = User.find_by_uuid(params[:to_uuid])
original_message = Message.find_by_uuid(params[:message_uuid])
# and perform some processing
# remember that at this point you have access to a Mailman "message" method which returns Mailman message object - you can get all details of the incoming email from it
end
end

In Mailman, you can create a new reply-message and also send its copy by e-mail. This way, you’ll implement such a system where users can exchange messages with each other directly from their e-mail clients, and in the meantime, there will be created messages on your web site.

As a bonus, you’ll get the capability to collect user’s alternative e-mails. Some users can have e-mail forwarding enabled in their mailboxes, so the actual reply can come from an e-mail address that is not present in your database. It allows you to implement user sign-in based on an alternative e-mail apart from the one that a user provided during a registration.

You should also notice that if you have to put some users in the CC or BCC fields, you will not be able to recognize a user who utilized an alternative e-mail to reply to your message. It will happen, because you will be able only to put author’s universally unique identifier (uuid) to the e-mail Reply-to address, while putting recipient’s uuid will not be possible due to a lot of CC recipients, which will not make sense. So, when a user will employ an alternative e-mail, you will just not have it in the database. In this case, you’ll have to determine who sent a reply message only based on the “From” field of the incoming e-mail.

Ok, at this point I stop. I hope this post was useful to you. Thanks for reading!

 

Further reading

  •  
  •  
  •