Friday, December 7, 2007

Sending GMail from a Standalone Ruby Script

As part of my rails application, I need to send emails from a stand alone ruby script. For example, the back end of my rails application is a (currently) small postgres database. The easiest way to back it up is to send a database dump to myself via gmail.

There are several tutorials on how to send gmail directly from a rails app. Presented here is a method for doing it from a standalone script. This method assumes rails is installed, but the script itself runs outside of any rails application. In my case in the wee hours of the morning as a cron job.

Usage of this script would be something like:
require 'send_gmail'
hsh={:to=>'name@domain.com', :subject=>'subject', :body=>'body'}
SendGMail.send_gmail(hsh)

The script can also handle attachments, albeit in an unusual way. Instead of giving it a file, you give it the data directly, along with a filename, mime_type, and transfer encoding:
raw_attachment={:mime_type=>'application/x-gzip', :body=>make_body, :transfer_encoding=>'base64', :filename=>'backup.tar.gz' }
hsh[:raw_attachment]=raw_attachment

It should be easy enough to modify the script to handle files as attachments directly, but I didn't have such a need.

Here is the complete listing. My thanks to Stephun Chu, as most of this is cribbed from his aforementioned post.

Edit: Fixed syntax error - Thanks Nolan!  

send_gmail.rb


#!/usr/bin/env ruby
require 'rubygems'
gem 'actionmailer'
require 'action_mailer'
require 'openssl'
require 'net/smtp'

module SendGMail

@user_name='username@domain.com'
@domain='domain.com'
@password='password'


def SendGMail.send_gmail(hsh)

raw_attachments=hsh.fetch(:raw_attachements, [])
if hsh.has_key?(:raw_attachment)
raw_attachments.push(hsh[:raw_attachment])
end

mail=TMail::Mail.new
mail.to=hsh[:to]
mail.date=Time.now
mail.from=@user_name
mail.subject=hsh[:subject]

main=mail
main=TMail::Mail.new
main.body = hsh[:body]
puts main.body
main.set_content_type('text/plain', nil, 'charset'=>'utf-8')
mail.parts.push(main)

for raw_attachment in raw_attachments
part = TMail::Mail.new
transfer_encoding=raw_attachment[:transfer_encoding]
body=raw_attachment[:body]
case (transfer_encoding || "").downcase
when "base64" then
part.body = TMail::Base64.folding_encode(body)
when "quoted-printable"
part.body = [body].pack("M*")
else
part.body = body
end

part.transfer_encoding = transfer_encoding
part.set_content_type(raw_attachment[:mime_type], nil, 'name' => raw_attachment[:filename])
part.set_content_disposition("attachment", "filename"=>raw_attachment[:filename])
mail.parts.push(part)
end

mail.set_content_type('multipart', 'mixed')
ActionMailer::Base.deliver(mail)

end

ActionMailer::Base.smtp_settings = {
:address => 'smtp.gmail.com',
:domain => @domain,
:authentication => :plain,
:port => 587,
:user_name => @user_name,
:password => @password
}

Net::SMTP.class_eval do
private
def do_start(helodomain, user, secret, authtype)
raise IOError, 'SMTP session already started' if @started
check_auth_args user, secret, authtype if user or secret

sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
@socket = Net::InternetMessageIO.new(sock)
@socket.read_timeout = 60 #@read_timeout
@socket.debug_output = STDERR #@debug_output

check_response(critical { recv_response() })
do_helo(helodomain)

raise 'openssl library not installed' unless defined?(OpenSSL)
starttls
ssl = OpenSSL::SSL::SSLSocket.new(sock)
ssl.sync_close = true
ssl.connect
@socket = Net::InternetMessageIO.new(ssl)
@socket.read_timeout = 60 #@read_timeout
@socket.debug_output = STDERR #@debug_output
do_helo(helodomain)

authenticate user, secret, authtype if user
@started = true
ensure
unless @started
# authentication failed, cancel connection.
@socket.close if not @started and @socket and not @socket.closed?
@socket = nil
end
end

def do_helo(helodomain)
begin
if @esmtp
ehlo helodomain
else
helo helodomain
end
rescue Net::ProtocolError
if @esmtp
@esmtp = false
@error_occured = false
retry
end
raise
end
end

def starttls
getok('STARTTLS')
end

def quit
begin
getok('QUIT')
rescue EOFError, OpenSSL::SSL::SSLError
end
end
end
end

13 comments:

nolan said...

I suppose that's a typo.
":domain => @domain.com,"
Great script, I couldn't figure out how to get ssl to work.

jyoti said...

Nice code..

but i try to attach file of 1.5MB (.rar extension) file and send it to a email address.

Email is working fine.I am receiving the mail but its showing the attached file size as 1Kb..when I decompress I can't able to do that..

Any suggestion ????
How will i attach the file and email it ???

Anonymous said...

In send_gmail try changing

raw_attachments=
hsh.fetch(:raw_attachements, [])

to:
raw_attachments = []

and attachments should work. At least for one attachment. But from there you should be able to figure the rest out.

Anonymous said...

And in the last comment change all misspellings of "attachement" to "attachment"

jyoti said...

Thanks for your suggestion.

But still I am facing the same problem.

One interesting issue i found is whatever the name of file i am giving for attachment its taking that as attachment irrespective of that file existence.

My Code is like this raw_attachment={:mime_type=>'application/pdf', :body=> 'make_body', :transfer_encoding=>'base64', :filename=> "somename.pdf" }
hsh[:raw_attachment]=raw_attachment

I did some mistake in above code as i am not able to understand what the ":body=>" is doing.i think its the data part..may be anyway

MY requirement is

script to handle files as attachments directly

jyoti said...

I want to send a .rar file as attachment.(files as attachments directly)
Means i will specify the path and the filename the script will automatically take that file as attachment.


Thanks in Advance

Otto said...

When I put both my script and send_gmail.rb in my home dir and call mine, everything works fine.

But when I try to make that a cron task, I get an error when I call SendGMail.send_gmail I get the following error: "Broken pipe".

What could be that?

Otto said...

Some puts here, some exception handling there, and I found out the error occurs in this exact line:
"ActionMailer::Base.deliver(mail)"

Error is "broken pipe".


What is the reason for that? It only happens when called by cron.
Thanks in advance.

Matt Di Pasquale said...

Thank you. it works! (didn't try the attachment feature yet... not needed at the moment) Very Easy! So cool! I love you!

Vincent said...

Thanks SO MUCH !!
Finally an easy way to do it without having to understand how it works !

Jia Zhang said...

Cool! I have tried another gem ruby-gmail but failed and troubleshooting for a lot of time. Your program saved my ass:)

Anonymous said...

Thank you so much. I've tried a few things and only yours work with Ruby 1.9. Awesome!
Alex

Anonymous said...

The formatting of your listing is that it cannot be copied. Could you provide a text-only version of your script?