Receiving Emails and Attachments with Rails

written by dnaffis on August 14th, 2006 @ 06:34 AM

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

  • Adam on 17 Apr 13:21

    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
  • Dave on 17 Apr 13:21

    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.
  • Dave on 17 Apr 13:21

    Adam, it makes a difference if the file (I'm assuming an image) is inline or attached. Something to consider.
  • Adam on 17 Apr 13:21

    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
  • Science on 21 Oct 11:50

    You’re not the only one stumped about inline attachments:

    http://dev.rubyonrails.org/ticket/6758

Comments are closed

Options:

Size

Colors