Receiving Emails and Attachments with Rails
Does your Rails app need to handle incoming emails with the attachments? All of the examples I’ve seen so far show you how to insert email attachments into the DB. Here’s a quick example that uses RailsCron to poll a POP3 account every minute for new emails and stores the attachments on the filesystem. If you need help using or running RailsCron see my previous posts about the topic.
The Agile book has a good example that kicks off a runner script but I think this method is far more efficient than having your mail system kick off a separate runner every time a new email is received, especially if you’re dealing in high volume.
It also handles non-responsive or slow responding POP3 servers by setting a high timeout length and retrying a handful of times before it gives up.
This class will check for and hand off any incoming emails:
require 'net/pop'
class EmailQueue < ActiveRecord::Base
background :poll_mail, :every => 1.minute, :concurrent => false
def self.poll_mail
retrycount = 0
begin
timeout(600) do
Net::POP3.start("yourdomain.com", nil, "username", "password") do |pop|
if pop.mails.empty?
logger.info "NO MAIL"
else
pop.mails.each do |email|
begin
logger.info "receiving mail..."
AssetSubmitHandler.receive(email.pop)
email.delete
rescue Exception => e
logger.error "Error receiving email at " + Time.now.to_s + "::: " + e.message
end
end
end
end
end
rescue TimeoutError
if(retrycount < 5)
retrycount+=1
retry
else
logger.info("ERROR Timeout error in poll_mail attempt #" + retrycount.to_s)
nil
end
end
rescue Exception => exception
SystemNotifier.deliver_exception_notification(exception)
logger.info("Error in poll_mail")
logger.info(exception.class.to_s + " " + exception.message.to_s + " " + exception.backtrace.to_s)
end
end
And here’s the code that handles the email:
class AssetSubmitHandler < ActionMailer::Base
# content type should be validated to image/gif, image/jpg, or image/jpeg
def receive(email)
if email.has_attachments?
email.attachments.each do |attachment|
asset = Asset.new
asset.submitter = email.from.first
asset.name = base_part_of(attachment.original_filename)
asset.content_type = attachment.content_type.chomp
base_dir = "/home/someapp/www/"
# save original file
asset.original = "assets/o_#{Time.now.utc.to_i}#{rand(1000000)}."+asset.name
File.open(base_dir+asset.original,File::CREAT|File::TRUNC|File::WRONLY,0666){ |f|
f.write(attachment.read)
}
asset.save
end
end
end
def base_part_of(file_name)
name = File.basename(file_name)
name.gsub(/[^W._-]/, '')
sanitize_filename(name)
end
# Fixes a 'feature' of IE where it passes the entire path instead of just the filename
def sanitize_filename(value)
#get only the filename (not the whole path)
just_filename = value.gsub(/^.*(\\|\/)/, '')
just_filename.gsub(/[^\w\.\-]/,'_')
end
end
Comments
-
David, I'm having some problems geting this to work. After I finish writing the file I open it up but its just garbage and the file size is much smaller than the original file that I attached in the email. Do I need to do a base 64 decode on the attachment? I couldn't find any library functions in ActionMailer to do this and I couldn't figure out how to apply the Base64.decode64 function to the attachment. adamflesher@hotmail.com
-
Adam, You should check the Content-Transfer-Encoding to see how it's encoded. If it is base64 then take a look at TMail::Base64 to handle the decoding. http://railsmanual.org/module/TMail%3A%3ABase64/1.1.2 Let me know if that helps.
-
Adam, it makes a difference if the file (I'm assuming an image) is inline or attached. Something to consider.
-
Dave, Thanks for your response. The decode method for the TMail::Base64 library wants to take a string as the parameter. The only way I could think of getting the decode method to accept the attachment was: TMail::Base64.decode(attachment.read.to_s) But it just returned garabage (no real surprise) and the file size is even smaller than before. To answer your question I'm just working with email attachments, nothing inline. Any insight on how to do this would be greatly appreciated as I seem to be really stumped. Thanks, Adam
-
You’re not the only one stumped about inline attachments:
http://dev.rubyonrails.org/ticket/6758
