Y.A.B.

Mike Mondagon

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

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