Yesterday, I spent the day working on a problem involving emails. Basically, it can be summed up as follows: the Rails application I'm working on has certain model objects that need to be able to have some data populated via emails (an email gets sent to a certain address, and the app processes it and populates data based on the content).
Now I'd read about ActionMailer before, I knew that it had a nifty way to receive email messages and process them as they arrive, so I went and looked at their
documentation on how to tackle that problem.
The most advocated method of handling the incoming email is to configure the Postfix server on you box to forward each email into your mailer script as it comes in, but I'm not a fan of that path for two reasons. First, I don't have any desire to worry about configuring a mail server. That probably makes me less "hardcore" in the eyes of any *nix masters out there, but given our time constraints I am forced to accept that I'm just not strong enough in that area yet to trust a business's success to it. Additionally, and probably more importantly, using a real-time forwarding approach means kicking up a new rails process EVERY TIME an email comes in. What if I get hundreds of emails in a single hour? Thousands? All of a sudden, crashing my server becomes as easy as spamming it's mailbox. No, there must be a better way.
And there is. Rather than try and accept each email as it comes in, you could poll the email box with POP or IMAP every so often and thus use one session to process however many emails have arrived in the time since the last session. To me this was obviously superior, and after checking several blogs of other ruby community members I was ready to give it a shot.
So I started a gmail account for ease of testing, and wrote a script that looked something like this:
pop = Net::POP3.new("pop.gmail.com", port)
pop.enable_ssl
pop.start('YourAccount', 'YourPassword')
if pop.mails.empty?
puts 'No mail.'
else
i = 0
pop.each_mail do |m|
File.open("inbox/#{i}", 'w') do |f|
f.write m.pop
end
m.delete
i += 1
end
puts "#{pop.mails.size} mails popped."
end
pop.finish
And this probably would have worked fine if not for one thing. For my purposes, I need SSL to be enabled, and the method on line 2 of the code example above ("enable_ssl"), is an enhancement made in ruby 1.9. Unfortunately, that's a development release, not yet production ready, and 1.8.6 (the current stable version, which I'm using) doesn't have ssl support in it's POP library.
I briefly thought adding the functionality to the 1.8.6 source code myself, but decided that would have to be a last resort tactic. Instead I switched to the IMAP library to see if I'd have any better luck. Here would be a typical code sample you'd see for fetching mail with the ruby IMAP library:
imap = Net::IMAP.new('imap.gmail.com')
imap.authenticate('LOGIN', 'username', 'password')
imap.select('INBOX')
imap.search(['ALL']).each do |message_id|
msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822']
MailReader.receive(msg)
imap.store(message_id, "+FLAGS", [:Deleted])
end
imap.expunge()
This looked promising, but every time I tried to run it I got a very strange error back from the server. Basically, the script would die and print out the words "Not Supported" and a filename coming back from the authentication request (line 2 above, imap.authenticate). I went and looked at the docs, and saw that the authenticate method supports 2 types of authentication: 'LOGIN' (like above), and 'CRAM-MD5', either one can be passed in as the first argument, and further more some servers won't support one or the other.
Sure I had found my fix, I replaced 'LOGIN' above with 'CRAM-MD5', and in an anticlimactic script run I found I got the exact same error.
If you're having this problem when trying to connect to gmail, I'm going to save you some time. The authenticate method in the IMAP library sends an "AUTHENTICATE" IMAP command to the server, which gmail does not support (you can prove this by sending a "CAPABILITY" command to the server, and seeing what options it has available. AUTHENTICATE ain't there).
Instead, using the "login" method (which sends no specific command), you can connect successfully. Here's my final script:
imap = Net::IMAP.new(@config['server'],@config['port'],true)
imap.login(@config['username'], @config['password'])
imap.select('INBOX')
imap.search(["NOT", "DELETED"]).each do |message_id|
MailFetcher.receive(imap.fetch(message_id, "RFC822")[0].attr["RFC822"])
imap.store(message_id, "+FLAGS", [:Deleted])
end
imap.logout()
imap.disconnect()
Enjoy!