EMIKETIC Logo

EMIKETIC Blog


EMIKETIC is a team of passionate programmers who fancy real-time and distributed application development. We write here about Meteor, React, modern JavaScript, Elixir, Phoenix and Ruby on Rails.


Receiving emails in ruby on rails applications

Ruby on Rails is well documented, easy to go web framework. But, what happens when you try developing a feature “out of the convention” that requires some advanced level of understanding of the framework?

What I kept in mind when I read Action mailer documentation at action_mailer was the “complex endeavor…”. So, I googled “receiving emails in rails”. Results are the same for you, I’m not sure but…

My web searches didn’t disappoint me, I guess, Griddler does the job for most of the cases. However, our inbox was a bit off the ordinary, especially when it comes to custom configuration, alongside our client. For example, each client needs to configure their pop3/imap parameters inside the application. Hence, We rejected Griddler method since it needs a configured mx record for the email server within the domain provider. Upset? Never I say.

Along this tutorial, we will be using Mailman, an incoming mail processing microframework written in Ruby. First, follow the instructions and install the gem in your bundle.

Part 1: What should be done?

Generate a new mailer with ‘rails generate mailer UserMailer’. This is where messages are handled depending on your needs. In our case, we simply persisted the messages. For this purpose, Message is a rails model with body:text and email:string. Put your hands up and generate the model with rails generate message body:text email:string. Now, in your mailer class, define your method ‘receive’:

def receive(message)
  message_id = message.subject[/^update (\d+)$/, 1]
	if message_id.present?
		part_to_use = message.html_part || message.text_part || message
		Message.update(message_id, body: part_to_use.body.decoded)
	else
		Message.create! subject: message.subject, body: body, email: message.from
	end
end

This method looks first for the message if it is present and updated it. If not, it creates a new one. To correctly parse and encode the body, we had to add the following:

 ...
else
	body = ""
	message.parts.each do |part|
	  next unless part.content_type =~ /^text\/html/
	  body << part.decode_body
	end
	Message.create! subject: message.subject, body: body.force_encoding('UTF-8'), email: message.from
end

Part 2: How should it be done?

Now that we know what to do with the message, let’s process it. So, under the script directory, create a script called mailman_server.rb:

#!/usr/bin/env ruby

require 'rubygems'
require 'bundler/setup'
require 'mailman'

require File.expand_path(File.dirname(__FILE__) + '/../config/environment')

#Mailman.config.logger = Logger.new("log/mailman.log")  # uncomment this if you can want to create a log file
Mailman.config.poll_interval = 5  # change this number as per your needs. Default is 60 seconds

Mailman.config.pop3 = {
  server: 'pop.gmail.com', port: 995, ssl: true,
  username:  "GMAIL_USERNAME",
  password:  "GMAIL_PASSWORD"
}

Mailman::Application.run do
  default do
    begin
    UserMailer.receive(message)
    rescue Exception => e
      Mailman.logger.error "Exception occurred while receiving message:n#{message}"
      Mailman.logger.error [e, *e.backtrace].join("n")
    end
  end
end

As an example, we used gmail pop3 configuration. If you decide to go with imap, don’t forget to change the settings in your Gmail for redirection. Inside the Mailman::Application, lies what we call routes. Inside Default route, we have access to the message object which is an instance of the Mailman::Message. Then, We can call our behaviour from the UserMailer method receive. It is time to test it out by running in the console bundle exec mailman_server.rb. You should see something like:

I, INFO -- : Mailman v0.7.3 started
I, INFO -- : POP3 receiver enabled (stoufa.turki@gmail.com@pop.gmail.com).
I, INFO -- : Polling enabled. Checking every 5 seconds.
I, INFO -- : Got new message from 'username@gmail.com' with subject 'Subject'.

Part 3: What if this is not exactly what we want?

All what we have seen previously was blogged before and screen-casted here at mailman-webcast.

Let’s discuss what we want. We want freedom. We want every client of the application (for example a saas) be able to configure their own email settings. To do this, we created a new model configs where all configuration (server, username, password_encrypted…) and the corespondent user is held. Look at the password column, how do you feel? Scared, Hah! Don’t worry. Generate the model for now and keep it going like a waterfall.

Regarding the encryption, we used symetric encryption openssl to encrypt/decrypt passwords. This cheatcode is a good place to understand: openssl-cheat. So to encrypt the password, we defined the following helper:

require 'openssl'
require 'base64'

module Mailer
	module SettingsHelper

		def encypt_password(password)
			cipher = OpenSSL::Cipher::AES.new(128, :CBC)
			cipher.encrypt
			cipher.key = Rails.application.secrets.secret_ssl
			password_encryped = cipher.update(params[:password]) + cipher.final
			Base64.strict_encode64 password_encryped
		end

	end
end

But what is the Base64 doing there? It encodes the binary data after encryption to return a ready to persist data format. Regarding Rails.application.secrets.secret_ssl, it is the new way to store secrets in Rails 4.2. For more details, see rails-upgrade

Our helper is ready, we can use it within the controller:

	config.password_encrypted = encypt_password(params[:password_encryped])

Now that we have a configuration, we can retrieve it in our server and set it as the config for the Mailman::Application. And as we did for the encryption, we did for the decryption using the same secret.

...
BEGIN {
  def decrypt_password(password_encrypted)
    decipher = OpenSSL::Cipher::AES.new(128, :CBC)
    decipher.decrypt
    decipher.key = Rails.application.secrets.secret_ssl
    password_encrypted = Base64.strict_decode64 password_encrypted
    decrypted_password = decipher.update(password_encrypted) + decipher.final
  end

  def inbox_config
    Config.find_by user_id: ARGV[0]
  end
}

Mailman::Application.run do
  initialize do
    Mailman.config.pop3 = {
      server: 'pop.gmail.com', port: 995, ssl: true,
      username: inbox_config.username,
      password: decrypt_password(inbox_config.password_encrypted)
    }
  end
	...
end

ARGV[0] stands for the user_id parameter passed when starting the server. By the end, the complete mailman_server.rb:

#!/usr/bin/env ruby

require 'rubygems'
require 'bundler/setup'
require 'mailman'
require 'bcrypt'
require "openssl"
require "base64"

require File.expand_path(File.dirname(__FILE__) + '/../config/environment')

#Mailman.config.logger = Logger.new("log/mailman.log")  # uncomment this if you can want to create a log file
Mailman.config.poll_interval = 5  # change this number as per your needs. Default is 60 seconds

BEGIN {
  def decrypt_password(password_encrypted)
    decipher = OpenSSL::Cipher::AES.new(128, :CBC)
    decipher.decrypt
    decipher.key = Rails.application.secrets.secret_ssl
    password_encrypted = Base64.strict_decode64 password_encrypted
    decrypted_password = decipher.update(password_encrypted) + decipher.final
  end

  def inbox_config
    Config.find_by user_id: ARGV[0]
  end
}

Mailman::Application.run do
  initialize do
    Mailman.config.pop3 = {
      server: 'pop.gmail.com', port: 995, ssl: true,
      username: inbox_config.username,
      password: decrypt_password(inbox_config.password_encrypted)
    }
  end
  default do
    begin
    UserMailer.receive(message)
    rescue Exception => e
      Mailman.logger.error "Exception occurred while receiving message:n#{message}"
      Mailman.logger.error [e, *e.backtrace].join("n")
    end
  end
end

Start the server using the command bundle exec script/mailman_server.rb param1. param> is the user_id for the used configuration.

You can also create a daemon for running the server in the background by creating a new file called mailman_daemon.rb:

#!/usr/bin/env ruby
require 'daemons'

Daemons.run 'script/mailman_server.rb'

After installing the daemons gem, you are ready to go with bundle exec script/mailman_daemon.rb start -- param1. Note that the dashed separation before the params is important.

##Part 4: Cheers