Processing emails with Postfix and Rails

Posted by Dan Sosedoff on August 10, 2011

This is a short manual on how to setup postfix and rails application to receive and process email messages.

Stack:

  • Debian / Ubuntu Server
  • Postix
  • Ruby 1.9
  • Rails 3.0

Overview

You have an application where users get email notifications. And you want to allow them to reply directly to the email.
In order to do so, each email should have an unique (depends on situation) reply-to address. Usually its something like that:

P946d272cf7da4dd6b0cb613605bced65@yourdomain.com

This means that the mailserver you use should support dynamic/virtual email addresses and forwarding.

Postfix Configuration

First, you’ll need to install postfix (in not installed):

apt-get install postfix

Configuration should look like this:

# See /usr/share/postfix/main.cf.dist for a commented, more complete version

default_privs = apps

# Debian specific:  Specifying a file name will cause the first
# line of that file to be used as the name.  The Debian default
# is /etc/mailname.
#myorigin = ap

smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no

# appending .domain is the MUA's job.
append_dot_mydomain = no

# Uncomment the next line to generate "delayed mail" warnings
#delay_warning_time = 4h

readme_directory = no

# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_use_tls=no
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client.

myhostname = YOUR_APP_HOSTNAME
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydomain = YOUR_APP_DOMAIN
mydestination = YOUR_APP_DOMAIN, localhost
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
relay_domain = localhost
recipient_canonical_maps = regexp:/etc/postfix/recipient_canonical

Last option recipient_canonical_maps allows you to define a dynamic email addresses and forward them to the specific system mailbox for processing.

Create a file /etc/postfix/recipient_canonical:

/^P[0-9abcdef]{1,}(M[0-9]{1,})?/ apps

This will add a virtual recipient addresses and forward messages to user apps.

NOTE: Regular expressions should be in POSIX format. For test you can use regextester.com

Email Aliases

After you added support for virtual addresses all mail will be delivered to the system user mailbox (apps). But, we need to drive all that traffic into our app. In order to do so we will have to setup mail piping directly into your application script.

Edit /etc/aliases:

apps: "| /home/apps/APP_NAME/current/script/email_receiver_script"

And rebuild the aliases db by running:

newaliases

Do not forget to restart postfix:

/etc/init.d/postfix restart

You can test out the email delivery. For errors check /var/log/mail.info

Mail Receiver Script

Since all mail will be forwarded directly to our mail receiver script via piping there are few things to consider:

  • Email receiver should consume as less memory as possible.
  • Email receiver should not load the whole application (because of item above).
  • Email receiver should only validate and preprocess incoming messages and leave actual processing to another subsystem via queue.

Configuration

There are few ruby libraries that are well suited for this case:

  • mail – Email processing, ruby 1.9.2 compatible (comparing to tmail which is not)
  • redis – Simple key-value in-memory database.
  • resque – Redis-backed library for creating background jobs, placing those jobs on multiple queues, and processing them.

Install gems:

gem install mail redis resque

Here is an example email receiver script:

#!/usr/bin/env ruby 

require 'rubygems'
require 'mail'
require 'redis'
require 'resque' 

class EmailReply
  @queue = :email_replies 

  def initialize(content)
    mail    = Mail.read_from_string(content)
    from    = mail.from.first
    to      = mail.to.first 

    if mail.multipart?
      part = mail.parts.select { |p| p.content_type =~ /text\/plain/ }.first rescue nil
      unless part.nil?
        message = part.body.decoded
      end
    else
      message = part.body.decoded
    end 

    unless message.nil?
      Resque.enqueue(EmailReply, from, to, message)
    end
  end
end 

EmailReply.new($stdin.read)

This script receives the mail message then tries to extract the plaintext body. If the email message is valid it adds it to the queue for future processing.

Mail Queue processing

After we put emails into the queue we’ll need to create a worker.

If you need to extract a reply from the body, use (mail_extract)[https://github.com/sosedoff/mail_extract]:

gem install mail_extract

Simple worker (resque job worker), extracted from one of the projects. (RAILS_ROOT/lib/email_reply.rb):

class InvalidReplyUUID    < StandardError ; end
class InvalidReplyUser    < StandardError ; end
class InvalidReplyProject < StandardError ; end
class InvalidReplyMessage < StandardError ; end 

class EmailReply
  @queue = :email_replies 

  def self.parse_email_uuid(str)
    if str =~ /^P[0-9abcdef]+(M[\d]+)?@/i
      parts = str.scan(/^P([0-9abcdef]+)(M([\d]+))?/).flatten
      project_uuid = parts.first
      message_id = parts.size == 3 ? parts.last : nil 

      result = {:project_uuid => project_uuid}
      result[:message_id] = message_id unless message_id.nil?
      result
    else
      raise InvalidReplyUUID, "Invalid UUID: #{str}"
    end
  end 

  def self.perform(from, to, body)
    user = User.find_by_email(from)
    if user.nil?
      raise InvalidReplyUser, "User with email = #{from} is not a member of the app."
    end 

    info = parse_email_uuid(to) 

    project = Project.find_by_uuid(info[:project_uuid])
    if project.nil?
      raise InvalidReplyProject, "Project with UUID = #{info[:project_uuid]} was not found."
    end 

    if info.key?(:message_id)
      message = project.messages.find_by_id(info[:message_id])
      if message.nil?
        raise InvalidReplyMessage, "Message with ID = #{info[:message_id]} was not found on project '#{project.name}'"
      end
    end 

    params = {
      :project  => project,
      :body     => MailExtract.new(body).body,
      :markup   => 'plain',
      :sent_via => 'email'
    }
    params[:message] = message unless message.nil? 

    message = user.messages.new(params)
    unless message.save
      raise RuntimeError, "Unable to save message. Errors: #{message.errors.inspect}"
    end
  end
end

NOTE: Its important that both mail receiver and worker are using the same queue.

Create a resque.rake in RAILS_ROOT/lib/tasks:

require 'resque/tasks'
task "resque:setup" => :environment

And fire it up:

rake resque:work QUEUE=email_replies

Dynamic settings for Ruby/Rails applications

Posted by Dan Sosedoff on February 07, 2011

Few times i needed to build dynamic-settings support into the application, which means that users (admins) can redefine website parameters like html keywords, notification email adresses and other simple data, that cannot be put into application environment settings. So, i extracted a small helper that will give me such ability across multiple apps – AppConfig

AppConfig is a library to manage your (web) application dynamic settings with flexible access and configuration strategy. Primary datasource for AppConfig is an ActiveRecord model.

Installation

git clone git://github.com/sosedoff/app-config.git
cd app-config
gem build
gem install app-config-x.y.z.gem

Data Formats

You can use following formats:

  • String
  • Boolean
  • Array
  • Hash

String format is a default format. Everything is a string by default.

Boolean format is just a flag, values ‘true’, ‘on’, ‘yes’, ‘y’, ‘1’ are equal to True. Everything else is False.

Array format is a multiline text which is transformed into array. Each evelemnt will be trimmed. Empty strings are ignored.

Hash format is special key-value string, “foo: bar, user: username”, which is transformed into Hash instance. Only format “keyname: value, keyname2: value2″ is supported. No nested hashes allowed.

Usage

AppConfig is designed to work with ActiveRecord model. Only ActiveRecord >= 3.0.0 is supported.
By default model “Setting” will be used as a data source.

Here is default structure:

ActiveRecord::Schema.define do
  create_table :settings do |t|
    t.string :keyname
    t.string :value
    t.string :value_format
  end
end

Now, configure:

AppConfig.configure

If your settings model has a different schema, you can redefine columns:

AppConfig.configure(
  :model  => Setting,           # define your model as a source
  :key    => 'KEYNAME_FIELD',   # field that contains name
  :format => 'FORMAT_FIELD',    # field that contains key format
  :value  => 'VALUE_FIELD',     #field that contains value data
)

Load all settings somewhere in your application. In Rails it should be initializer file.

AppConfig.load

AppConfig gives you 2 ways to access variables:

AppConfig.my_setting      # method-like
AppConfig[:my_setting]    # hash-like by symbol key
AppConfig['my_setting']   # hash-like by string key

You can define settings items manually. NOTE: THESE KEYS WILL BE REMOVED ON RELOAD/LOAD.

AppConfig.set('KEYNAME, 'VALUE', 'FORMAT')

Everytime you change your settings on the fly, use reload:

AppConfig.reload

Cleanup everything:

AppConfig.flush

Github source: https://github.com/sosedoff/app-config

Discover hidden API services with Proxie

Posted by Dan Sosedoff on January 25, 2011

Need to figure out closed/private API? Not a problem. Its always fun and challenging. There are few tools i use for that:

Proxie is another project of mine, which i finally extracted from a script written a while ago and published it as a ruby gem.
I used it to track Grooveshark API and it worked out great. Here is the list of features:

  • Bind proxy server to any port (default is 8080)
  • Define output database. It uses SQLite3 by default, but you can extend it with any other types
  • Sinatra-base web interface to browse all your collected data

Installation:

sudo gem install proxie

Start a server:

proxie -d DATABASE_NAME

After data collection just run:

proxie --web

The web interface runs on localhost:4567 by default.

Usage summary:

Usage: proxie [options]
    -i, --info                       Display this information.
    -p, --port PORT                  Listen on port (8080 default)
    -d, --db NAME                    Store results to database
    -w, --web                        Start a Web UI for databases
    -f, --flush                      Delete all local databases

Project on GitHub: http://github.com/sosedoff/proxie