A Ruby Gateway for Postfix (in order to build a Rails Webmail)


Edit : This code is globally out of date and pretty hazardous... I now use the awesome Astrotrain plugin to retrieve E-Mails into Ruby on Rails' apps, but feel free to read my blog post anyway :-)

Well, it took me almost two weeks to get the whole thing to work, and regarding to the poorly documentation I have had to deal with, I think it could be a good idea to share this experience.

The goal is to retrieve incoming and outgoing mail from Postfix, then push them into a Database. First of all, let’s configure Postfix. I assume you already have a working installation of Postfix. Postfix uses the “vmail” user (yes, I use the MySQL virtual Mail package).

Add this to your /etc/postfix/master.cf :

mail_filter unix -     n       n       -       -       pipe
 flags=hq user=vmail argv=/usr/bin/ruby1.8 /var/spool/filter/mail_filter.rb

smtp      inet  n       -       -       -       -       smtpd
 -o content_filter=mail_filter:

Now, every mail (incoming AND outgoing!) will be “piped” to the /var/spool/filter/mail_filter.rb Ruby script. By the way, I could -of course- have used any language like Shell or Perl, but I read some interesting docs about Tmail, and I now really love Ruby, so… why not ?

First, you’ll need a Mail parser to read the mail content : install TMail

sudo gem install tmail

Ok, let’s create this Ruby filter, we’ll store it where we just told to Postfix to look at

/var/spool/vmail/mail_filter.rb

And now, let’s read mails !

#Read the message
message = $stdin.read
#Parse it
mail = TMail::Mail.parse(message)

…easy, hu ? Now, let’s see what’s inside this mail

#Extract what we need...
from_list = "" #the "from" field is also an array 
mail.from.each do |addr|
  from_list.length > 1 ? from_list << addr + "," : from_list << addr
end        

to_list = ""
mail.to.each do |addr|
  to_list.length > 1 ? to_list << addr + "," : to_list << addr
end

if mail.cc != nil 
  cc_list = ""
  mail.cc.each do |addr|
    cc_list.length > 1 ? cc_list << addr + "," : cc_list << addr
  end
end

if mail.bcc != nil
  bcc_list = ""
  mail.to.each do |addr|
    bcc_list.length > 1 ? bcc_list << addr + "," : bcc_list << addr
  end
end

Every mail now piped to the Mail Filter, WE DO HAVE to handle file writing if we want to retrieve mail using pop3 ! Skip this if you don’t want to duplicate mails to the filesystem and in the DB

#Get the domain name
@domain_name = to_list.split('@')[1]

#Write the mail to the file system so we still can access it via pop3
myFile = File.open("/var/spool/vmail/#{@domain_name}/#{to_list.split('@')[0]}/new/#{Time.now.to_i}", "w")
myFile.write message
myFile.close

DB Connection. I use the exact domain name for the database name as convention, this now allow me to link the mail to the proper database.

# connect to the MySQL server, Warning : The domain name MUST BE the same as DB name
begin
  dbh = Mysql.real_connect("localhost", "DB_user", "DB_password", @domain_name.split('.')[0])     
rescue
  Process.exit
end

And now we insert the mail body, assuming this is a simple text mail

if mail.parts.length == 0
  #no attachment, only a text body
  req = dbh.query("INSERT INTO emails (filename,from_field,to_field,cc_field,cci_field,subject,content,created_at,updated_at,status,direction,email_account_id) 
              VALUES (
              'nom de fichier',
              '#{from_list}',
              '#{to_list}',
              '#{cc_list}',
              '#{bcc_list}',
              '#{mail.subject}',
              '#{mail.body}',
              now(),
              now(),
              'new',
              'incoming',
              '1')
              ")

But, what about attachments ?! Hehe… this part is quite a bit F***** tricky and causes me some headaches because of the lack of documention

  
else
  #email.attachments are TMail::Attachment
  #but they ignore a text/mail parts.
  @i = 0
  @email_id = 0
  mail.parts.each_with_index do |part, index|
    if @i == 0
      #Push mail text body into DB
      req = dbh.query("INSERT INTO emails (filename,from_field,to_field,cc_field,cci_field,subject,content,created_at,updated_at,status,direction,email_account_id) 
                  VALUES (
                  'nom de fichier',
                  '#{from_list}',
                  '#{to_list}',
                  '#{cc_list}',
                  '#{bcc_list}',
                  '#{mail.subject}',
                  '#{part.body}',
                  now(),
                  now(),
                  'new',
                  'incoming',
                  '1')
                  ")
    else
      filename = (part['content-location'] && part['content-location'].body) || part.sub_header("content-type", "name") || part.sub_header("content-disposition", "filename")

      #get email_id and create the sub dir for attachments
      if @email_id == 0 
         @email_id = dbh.insert_id()
         system "mkdir /home/guillaume/#{@domain_name.split('.')[0]}/tmp/attachments/#{@email_id}"
         system "chmod -R 0770 /home/guillaume/#{@domain_name.split('.')[0]}/tmp/attachments/#{@email_id}"
      end
      
      dbh.query("INSERT INTO attachments (created_at,updated_at,filename,email_id,content_type)
                VALUES (
                now(),
                now(),
                '#{filename}',
                #{@email_id},
                '#{part.content_type}'
                )
      ")
      #write attachments to the file system
      File.open("/home/guillaume/#{@domain_name.split('.')[0]}/tmp/attachments/#{@email_id}/#{filename}",File::CREAT|File::TRUNC|File::WRONLY,0777) do |f|
        f.write(part.body)
        system "chmod 0770 /home/guillaume/#{@domain_name.split('.')[0]}/tmp/attachments/#{@email_id}/#{filename}"
      end    
    end
    @i += 1
  end
end

And voila ! As you can see, I store Attachments on the filesystem, this is my own way to do things as I do hate to insert huge blobs into Databases, but do what you want…

You can retrieve the whole script here and adapt it to you needs, hope this could help…


Envie de donner votre avis ?

Votre Nom*

Adresse Mail (ne sera pas publiée)*

Site web

Votre commentaire*


Cochez cette case si vous n'êtes pas un robot spammeur