Ruby on Rails
HowToReceiveEmailsWithActionMailer

Action Mailer can be configured to receive incoming email and interact with the domain model of the application on the basis of the content. A few examples of what that could be used for:

Let’s consider the following Action Mailer:

  class Mailman < ActionMailer::Base
    def receive(email)
      page = Page.find_by_address(email.to.first)
      page.emails.create(
        :subject => email.subject, 
        :body => email.body
      )

      if email.has_attachments?
        for attachment in email.attachments
          page.attachments.create({ 
            :file => attachment, 
            :description => email.subject
          })
        end
      end
    end
  end

When the Mailman receives an email, it’ll attach the body and subject text to the page through an Email object and create a number of attachment objects for each of the included files.

Configuring Postfix to forward the emails

In order to get the email, though, we’ll need to configure the UNIX email handler Postfix (see below if your system uses Sendmail). First, create an alias on the machine. We’ll call this alias mailman and add it like this (/etc/mail/aliases is a common path for this definition file, but on some systems it’s found in /etc/postfix/aliases):

mailman: "|/path/to/app/script/runner Mailman.receive(STDIN.read)"

note: I found that this seems to break at (least on linuxy systems) and is difficult to determine why without digging around the logs. The shell complains about the parens, so to fix, I had to quote the script parameter:

mailman: "|/path/to/app/script/runner 'Mailman.receive(STDIN.read)'"

Then we’ll install the new alias by running newaliases.

If you are using virtual hosts in Postfix, you need to inform Postfix that incoming emails of a given address should be forwarded to this alias (/usr/local/etc/postfix/virtual is a common location for this definition file, but on some systems it’s found in /etc/postfix/virtual):

/^.*@.*example\.com$/          mailman

Once again, this configuration needs to be activated, which is commonly done through cd /usr/local/etc/postfix && make && make install.

That’s it! Now we can send an email to somewhere@example.com and Mailman will receive it, instantiate the Page that has that address, and create an Email object to go in the emails collection and a variable number of Attachment objects to go into the attachments collection.

Configuring Postfix to Handle All Mail for a Domain

Postfix can fairly easily be set up such that all mail coming in to a particular address (for example, anything@lists.yourdomain.com) gets handed off to your ActionMailer handler. First, open the Postfix master.cf file (commonly found in /usr/local/etc/postfix/master.cf or /etc/postfix/master.cf) and add something similar to the following to the end:

mailman  unix  -       n       n       -       -       pipe
  flags= user=nobody argv=/path/to/ruby /path/to/app/script/runner Mailman.receive(STDIN.read)

That defines the mailman transport which will pass all its messages through the mail handler. Next, edit the main.cf file (it should be in the same place as master.cf) and add the following or change the existing settings if they’re there:

transport_maps = hash:/path/to/etc/postfix/transport
virtual_mailbox_domains = lists.yourdomain.com

This tells Postfix to look in the transport file for information on how to handle delivery for specific domains and that it should accept mail for “lists.yourdomain.com.” Finally, create or edit the transport file listed in transport_maps above so it contains something like:

lists.yourdomain.com    mailman:

This tells Postfix that all mail coming to “lists.yourdomain.com” is to be handled by the mailman transport, as defined in master.cf. Finally, rebuild the transport map with /usr/local/bin/postmap /path/to/etc/postfix/transport and then restart Postfix. You should be good to go.

Configuring Sendmail to forward the emails

If your system uses Sendmail as its Mail Transfer Agent (MTA) then here’s what you’ll need to do to configure it to handle messages. These instructions are intended for and based on Red Hat and similarly configured Linux distributions, so you may have to do some adaptation or translation appropriate to your local configuration. (I also include these instructions mostly for completeness since I personally use Postfix — JeffAbbott)

First, create an alias. We’ll call it “mailman” and you can add it to the aliases file (most commonly found in /etc/aliases) like so:

mailman: "| /path/to/ruby /path/to/app/script/runner -e production 'Mailman.receive STDIN.read'"

The -e option is used to specify the environment.

Then rebuild the aliases table by running the newaliases command.

Next, you’ll need to set it up so that smrsh (the Sendmail Restricted Shell, an increased security measure when using external programs to handle incoming mail) can run the ruby interpreter. Make a symbolic link as follows (skip this step if you don’t have an /etc/smrsh directory as that most likely means your system isn’t configured to use the Sendmail Restricted Shell):

ln -s /usr/bin/ruby /etc/smrsh/

That’s all you need to do if you just want your Action Mailer subclass to handle mails addressed to mailman@yourdomain.com. If you want all addresses at that domain to redirect to the mailman alias, then add the following to your virtusertable file (probably in the /etc/mail directory):

@yourdomain.com     mailman@localhost

Then rebuild the virtusertable database by running make -C /etc/mail and restart Sendmail by running /etc/init.d/sendmail restart. That’s pretty much all there is to it.

Configuring Qmail to forward the emails

If your system uses Qmail as its Mail Transfer Agent (MTA) then here’s what you’ll need to do to configure it to handle messages. This message assumes a default installation of qmail with a single domain.

Option 1: On a per user basis:

Add a user and edit their “dot-qmail” or .qmail file to pipe delivery to a program:

In linux/bsd that’s:

# useradd -m -G users -s /bin/false ruby-mail

This adds a user that has no login capabilities, with a simple home directory, and also part of the users group (the group is distro specific to gentoo and others).

Now edit /home/ruby-mail/.qmail to read:

| /usr/bin/ruby /path/to/your/script/runner 'IncomingMailHandler.receive(STDIN.read)'

This will pipe all e-mail sent to ruby-mail@yourdomain.com to your rails app.

You can also do name catch-alls, so that ruby-mail-order@yourdomain.com and ruby-mail-status@yourdomain.com can run different scripts. Refer to the man page for dot-qmail for the syntax.

Option 2: A domain-catch-all

You can also just forward all emails to your domain if you edit the site-wide qmail file called .qmail-default usually located in (or around):

/var/qmail/alias/

Placing the following in .qmail-default in the above directory will forward all incoming mail ( *@yourdomain.com ) to your ruby script. Be careful on a high-traffic server, as you may be getting some SPAM in there too.

| /usr/bin/ruby /path/to/your/script/runner 'IncomingMailHandler.receive(STDIN.read)'

Careful with permissions of the .qmail files (keep them the same) as the current directory, especially for the /var/qmail files.

Option 3: procmail/vpopmail

No real additions/gotchas here — qmail can pipe to procmail using several recipes, giving you a little more flexibility, with a little more redirection (qmail → procmail → ruby). If you pipe qmail to procmail, you can use the above scripts in different sections to perform procmail tricks like header/subject/etc. line filtering.

Pulling mails via POP3 into Rails

You can use a tool like getmail to get your mails into rails without the need of an local mailserver. The relevant getmail configuration is like this:

[rails]
type = MDA_external
path = /usr/bin/env
arguments = ("RAILS_ENV=production", 
 "sh", "-c", 
 "cd /usr/local/project/rails; /usr/local/bin/ruby 
 script/runner 'Incoming.receive(STDIN.read)'")

For some more detailed explanation see POPing mails into Rails

Receiving with ActionMailer on windows

For those of us working on windows, there’s a relatively painless way of simulating a mail delivery for testing, as well as a less painless way of actually receiving emails

Follow the instructions above for setting up ActionMailer. Save the email you want AM to receive as a text file, then open a command a prompt and go to your rails directory and enter:

ruby script/runner Mailman.receive(STDIN.read) < email.txt

email.txt is the name of the file you saved your email to, and if you didn’t save it in your rails directory you’ll have to provide the full path to it too.

It’s not ideal but useful for testing if you’re on windows.

Receiving with Net::POP3/IMAP

I’m not sure if there are issues with doing it this way, but I’m surprised that it hasn’t already been listed as a possibility. So, why not use Net::POP3 or Net::IMAP to check email in Rails? It seems to me the most straightforward. I’ll be implementing some features that require mail-checking soon, so I’ll post some code when that is done. Meanwhile, click the links to see the docs with examples for these libraries.

Example using Net::POP3

File: script/inbox
===========
require 'net/pop'
require File.dirname(__FILE__) + '/../config/environment'

logger = RAILS_DEFAULT_LOGGER

logger.info "Running Mail Importer..."
Net::POP3.start("mail.myserver.net", nil, "username", "secretpassword") do |pop|
  if pop.mails.empty?
    logger.info "NO MAIL"
  else
    pop.mails.each do |email|

      begin
        logger.info "receiving mail..."
        Mailman.receive(TMail::Mail.parse(email.pop))
        email.delete
      rescue Exception => e
        logger.error "Error receiving email at " + Time.now.to_s + "::: " + e.message
      end
      
    end
  end
end
logger.info "Finished Mail Importer."

If Mailman.receive(TMail::Mail.parse(email.pop)) gives you an error, try changing it to Mailman.receive(email.pop)

Example using Net::IMAP

Fetching the mailbox via IMAP is by far the best way of getting messages into Ruby. Piping them through procmail et. al. is very cumbersome, requires access to the unix filesystem and (not in the least) poses a huge security risk. What if someone drops a mailbomb on your system? Not a pretty sight. So, without much further ado, here’s the check_mail method, which you can add to your rails mailer class, to check the inbox on your local IMAP server, and deliver the messages to the receive method of the mailer class:


File: app/models/mail_reader.rb
===================

require 'net/imap'

class MailReader < ActionMailer::Base

  def receive(email)
    # use the mail object as you normally would
  end

  def self.check_mail
    imap = Net::IMAP.new('localhost')
    imap.authenticate('LOGIN', 'username', 'password')
    imap.select('INBOX')
    imap.search(['ALL']).each do |message_id|
      msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822']

      MailReader.receive(msg)
      #Mark message as deleted and it will be removed from storage when user session closd
      imap.store(message_id, "+FLAGS", [:Deleted])
    end
    # tell server to permanently remove all messages flagged as :Deleted
    imap.expunge()
  end

end

Run the above file every so often (from cron, for example) with this command:

ruby script/runner 'MailReader.check_mail'

What if you needed to authenticate with the ‘PLAIN’ type? How would that be handled using an example that includes the ‘add_authenticator’ method of IMAP?

see also Sending email with ActionMailer

Decoding attachments

The strategy to use is to enumerate over the TMail::Mail object’ parts to get each as a part (body text is not a part if enumerating over attachments). I show how to retain an attachment’s name and load a mail unit test fixture with attachments on my blog.

Decoding attachments revisited (3/19/07)

Actually, there’s a bug with the current code (as of Rails 1.2.3). TMail::Unquoter.unquote_base64_and_convert_to should be the below. Monkey patch Rails with this and you’re good to go with using the attachments array on a TMail::Mail instance. You can find the patch at http://dev.rubyonrails.org/ticket/7861


class TMail::Unquoter
  def self.unquote_base64_and_convert_to(text, to, from)
    convert_to(TMail::Base64.decode(text), to, from) # Removed ".first" after the decode
  end
end

Cheers, Dave Myron