decoding email attachments with ActionMailer::Base receive
There are lots of references that say you can decode images and other media attachements in email using the receive method of ActionMailer::Base
Ruby On Rails Wiki is one: HowToReceiveEmailsWithActionMailer.
Rails Recipes Recipe #68 “Testing Incoming Mail” is another.
But those references and many others on the internet don’t show you how to really do it.
Here’s how its done …
The key is to use the TMail::Mail email object’s parts to do your work.
Use the mailer generator to stub out your code:
ruby script/generate mailer HelloWorldMail
exists app/models/
create app/views/hello_world_mail
exists test/unit/
create test/fixtures/hello_world_mail
create app/models/hello_world_mail.rb
create test/unit/hello_world_mail_test.rb
The full files in my example are contained in this bzip archive: hello-world-mail.tar.bz2
See the hello-world.mail test fixture. It contains a real email file with a ruby logo image file as an attachment. The file goes in the hello_world_mail test fixture directory: test/fixtures/hello_world_mail/hello-world.mail
This is the snippet of code in the unit test that will have the HelloWolrdMail parse that raw file into a TMail::Mail object which is passed into the receive method:
def test_hello_world
email_text = read_fixture('hello-world.mail').join
HelloWorldMail.receive(email_text)
end
HelloWolrdMail decodes the mail and places files into the ‘tmp’ directory in your Rails root.
Here is all of the code of the HelloWolrdMail ActionMailer::Base so you don’t have to unzip the archive. The real work is to enumerate over the parts of the mail and then dump the decoded body to disk. TMail auto-magically decodes the content ecoding (7bit, 8bit, quoted-printable, base64, binary). When TMail parses out the attachments of a mail it doesn’t treat a text/plain part as a part. It does parse all of the content to parts but leaves out the original file name. So this is how I get the best of both worlds.
The text of the mail will be contained in #{RAILS_ROOT}/tmp/1.txt and the ruby-lang.org logo that was attached to the mail will be contained in #{RAILS_ROOT}/tmp/ruby.gif
Update 07/10/2007 See DANG Zhengfa’s post in the comments section for a cleaner code. My example was written when I was a bit more of a n00b. Also my code was dealing TMail before it was fixed in the ActionMailer in the latest 1.3.3 release.
Update 08/01/2007
Cleaned up the code with better Ruby idioms, when I first wrote it I was still unlearning Java. As PJ says uses attachment_fu if you just want to get at the content quickly. This code works with Rails 1.2.3 / ActionMailer 1.3.3 which automatically decodes the attachment’s content encoding.
class HelloWorldMail < ActionMailer::Base
# email is a TMail::Mail
def receive(email)
#email.attachments are TMail::Attachment
#but they ignore a text/mail parts.
email.parts.each_with_index do |part, index|
filename = part_filename(part)
filename ||= "#{index}.#{ext(part)}"
filepath = "#{RAILS_ROOT}/tmp/#{filename}"
puts "WRITING: #{filepath}"
File.open(filepath,File::CREAT|File::TRUNC|File::WRONLY,0644) do |f|
f.write(part.body)
end
end
end
# part is a TMail::Mail
def part_filename(part)
# This is how TMail::Attachment gets a filename
file_name = (part['content-location'] &&
part['content-location'].body) ||
part.sub_header("content-type", "name") ||
part.sub_header("content-disposition", "filename")
end
CTYPE_TO_EXT = {
'image/jpeg' => 'jpg',
'image/gif' => 'gif',
'image/png' => 'png',
'image/tiff' => 'tif'
}
def ext( mail )
CTYPE_TO_EXT[mail.content_type] || 'txt'
end
end
Posted in Nuby Rails, Rails, web2.0 |
Trackbacks<
Use the following link to trackback from your own site:
http://plasti.cx/trackbacks?article_id=219
03/15/2007 at 07:55AM
I see in your example that you are decoding everything with base64_decode. What happens if it is encoded differently?
03/15/2007 at 08:57PM
Good eyes. TMail auto-magically decodes the transfer encoding of a part’s body. So I updated my example. Thanks!
03/18/2007 at 11:31PM
My TMail just returns a body of 2 bytes for any attachment (including the one in your .tar). It seems that it’s not decoding the attachment when I call body (and looking at the code for #body shows that a non-multipart returns unquoted_body(to_charset) … to_charset defaults to ‘utf-8’.
Do I have an old version of TMail somehow?
05/12/2007 at 11:13PM
A simpler solution thanks to the attachment_fu plugin is:
That’s all there is to it.
07/10/2007 at 11:33PM
I have tried your code on the latest tmail. it does not work. I modifed your code, here it is:
Now you can use > ruby parser.rb myemail.dat
to parse all parts of email, recursively. All images & text will be sperated from the email properly.
07/10/2007 at 11:42PM
sorry for so many postings..
07/10/2007 at 11:56PM
I cleaned up the posts DANG, thanks for taking the time to clean up the code and present it so that others can learn from it.
08/01/2007 at 03:15AM
Hmmm, this is exactly what I need to do!! Extract a jpeg from an email. However, using Mr/Ms Dang’s version which gives lots of great feedback – however my jpeg attachment is resolved to a 2 byte file (the plain text and html parts of the multipart message are saved fine). The attachment is show with the right filename, base64 encoding and the right MIME type.
Am on Rails 1.2.3.7116.
Any thoughts or ideas on how to get past this hurdle?
Cheers, —Kip
08/01/2007 at 07:30AM
OK, I’ve worked it out. Apply the patch at http://dev.rubyonrails.org/attachment/ticket/7861/base64_decoding.patch, which means you don’t have to check for a part being in base64 (all back to automatic).
And then make sure the file is opened in binary mode (the code here doesn’t do that, at least for this windows machine).
Now all is good.
—Kip
08/01/2007 at 06:40PM
@KipCole did you say automatic or automagic? :)
And my original code has of the fresh smell of noob all over it, you get a free index on Enumerable’s with enum.each_with_index, so I updated the original code see the “update 08/01/2007” comments.
11/05/2007 at 10:55PM
decode automagically decodes the transfer encoding of mime parts now (again). This blog post was before TMail was fixed in Rails 1.2.2 back in around March of 2007. Also the official TMail with all of the Rails patches is being maintained again at Rubyforge
http://rubyforge.org/projects/tmail/
04/21/2008 at 03:04PM
Decoding mail via Rails is one of the neatest features I’ve seen.