Eric Anderson
4/11/2005 2:56:00 PM
Phil Tomson wrote:
> Has anyone written any Ruby code for accessing their gmail account that
> they would be willing to share?
>
> Here are the requirements according to google:
>
> pop.gmail.com
> Use SSL: Yes
> Port: 995
I have a bit of code that might help. I briefly tried Kapsules since it
had Ruby support but I found it to be unstable, slow and resource
intensive. But while I was using it I needed to get the Mail Kapsule
using SSL and since it was written in Ruby I added the necessary
support. Of course ruby-1.8 doesn't support SSL but the CVS version does
so I sort of backported it (my version is a bit simpler, therefore the
interface is different). The great thing is with Ruby this backport only
took about 1 hour! Anyway I have attached my modified mail kapsule file.
You don't need all the kapsule stuff. Just the stuff at the end where I
add added the SSL POP3 support. I included the entire script so you can
see how the code should be used (plus I'm too lazy to cut out just the
parts you need).
Eric
require 'socket'
require 'net/pop'
require 'net/imap'
require 'net/protocol'
require 'openssl/ssl'
def log( text )
File.open( Info.WidgetPath + "kapmail.log", "a" ) do |f|
f.puts( Time.now.strftime( "[%m/%d %H:%M:%S] " ) + text.to_s )
end
end
def Widget_Activate()
path = Info.WidgetPath() + "Resources/email.png"
$icon = Graphics.LoadImage( path )
$msg_ids = Hash.new
$seen = Hash.new
readConfiguration
Widget_Paint()
end
def Widget_Deactivate()
Widget.DeleteTimer "POPCheck"
$icon.Dispose
end
def Widget_ConfigChange()
readConfiguration
Widget_Paint()
end
def readConfiguration()
# Read the conf file.
$classes = Hash.new
$delete_regexps = Array.new
$servers = Array.new
lines = IO.readlines( Info.WidgetPath + Settings.GetSettingNode( "widgetroot/conf" ) )
lines.each do |l|
line = l.chomp
command, args = line.split( /\s+/, 2 )
case command
when "class"
cl, regexp_str = args.split( /\s+/, 2 )
if regexp_str != nil
$classes[ cl ] = Regexp.new( regexp_str )
end
when "server"
args =~ /^(.+?)\s+(.+?):(\d+)\s+(.+?);(.+)/
$servers.push( {
"protocol" => $1,
"host" => $2,
"port" => $3.to_i,
"username" => $4,
"password" => $5
} )
when "delete"
$delete_regexps.push( Regexp.new( args ) )
end
end
# Remaining configuration.
Graphics.Font.ColorHTML( Settings.GetSettingAttribute( "widgetroot/font", "color" ) )
Graphics.Font.Name = Settings.GetSettingAttribute( "widgetroot/font", "name" )
Graphics.Font.Size = Settings.GetSettingAttribute( "widgetroot/font", "size" )
# Setup the check interval.
Widget.DeleteTimer "POPCheck"
Widget.AddTimer( "POPCheck", Settings.GetSettingNode( "widgetroot/interval" ).to_i * 60 * 1000 )
# Sound file.
@soundfile = Settings.GetSettingNode( "widgetroot/sound" )
# Absolute or relative filepath?
if @soundfile != nil and @soundfile != ""
if @soundfile !~ /(?:[A-Za-z]:)?\\/
# Relative
@soundfile = Info.WidgetPath + @soundfile
end
end
end
def checkAgainstClasses( line )
$classes.each do |cl, regexp|
if line =~ regexp
$counts[ cl ] = ( $counts[ cl ] or 0 ) + 1
end
end
end
def Widget_Paint()
Graphics.Clear()
drawBackground
Graphics.DrawString( "(checking mail...)", 5, 5 )
Widget.UpdateWidget()
# ---------
$counts = Hash.new
num_lines = 1
num_msgs = 0
msg = ""
server_threads = Array.new
$servers.each do |server|
t = Thread.new do ||
protocol = server[ "protocol" ].downcase
host = server[ "host" ]
port = server[ "port" ]
username = server[ "username" ]
password = server[ "password" ]
log "Connecting to #{protocol} server #{host}:#{port} with username '#{username}'..."
begin
case protocol
when /pop/
kls = protocol =~ /spop/ ? Net::POP3S : Net::POP3
kls.start( host, port, username, password ) do |pop|
pop.mails.each do |m|
header = m.header.split( /\n/ )
# Get message ID.
id = nil
header.each do |l|
line = l.chomp
if line =~ /^Message-Id: <(.*)>/
id = $1
$msg_ids[ id ] = true
break
end
end
# Check for regexp matches.
result = checkDeletion( header )
if result != nil
subject, from = result
log "Deleting '#{subject}' from #{from}"
m.delete
else
if not $seen[ id ]
header.each do |l|
line = l.chomp
checkAgainstClasses( line )
end
num_msgs += 1
end
end
end
end
when /imap/
log "Connecting to #{host}:#{port}..."
imap = Net::IMAP.new( host, port )
log imap.capability().join( "|" )
#imap.authenticate( 'LOGIN', username, password )
imap.login( username, password )
#imap.examine( 'INBOX' )
imap.select( "INBOX" )
# Check for matches.
to_delete = Array.new
imap.search( [ "RECENT" ] ).each do |message_id|
uid = imap.fetch( message_id, "UID" )[ 0 ].attr[ "UID" ]
log "UID: #{uid}"
$msg_ids[ uid ] = true
header = imap.fetch( message_id, "RFC822.HEADER" )[ 0 ].attr[ "RFC822.HEADER" ].split( "\n" )
result = checkDeletion( header )
if result != nil
subject, from = result
log "Deleting '#{subject}' from #{from}"
to_delete.push uid
else
if not $seen[ uid ]
header.each do |l|
line = l.chomp
checkAgainstClasses( line )
end
num_msgs += 1
end
end
end
if to_delete.length > 0
imap.uid_store( to_delete, "+FLAGS", [:Deleted] )
end
end
rescue Errno::ECONNREFUSED => error
msg = "Connection to #{host}:#{port} refused.\n"
rescue Exception => e
msg = "CRITICAL EXCEPTION!\nShift-right-click in here to check your configuration."
log e.message
log "protocol: '#{protocol}' host: '#{host}' port: #{port} username: '#{username}'"
e.backtrace.each do |stack_element|
log stack_element
end
num_lines += 1
end
end
if Settings.GetSettingNode( "widgetroot/multithreaded" ) == "Yes"
t.priority = 2
server_threads.push t
else
t.join
end
end
main_thread = Thread.new do ||
server_threads.each do |t|
t.join
end
widest = 0
if $counts.length > 0
$counts.each do |cl, count|
sub_msg = cl + ": " + count.to_s
width = Graphics.GetStringWidth( sub_msg )
widest = width if width > widest
msg += sub_msg + "\n"
end
end
sub_msg = "Total: #{num_msgs}"
width = Graphics.GetStringWidth( sub_msg )
widest = width if width > widest
msg += sub_msg
# Bug in Kapsules? Workaround:
widest += 1
Graphics.Clear()
Widget.Width = widest + 20
Widget.Height = Graphics.GetStringHeight( msg, widest ) + 20
if num_msgs > 0
corner_x, corner_y = mailIconCorner
Widget.Width = [ Widget.Width, corner_x + 10 ].max
Widget.Height = [ Widget.Height, corner_y + 10 ].max
end
drawBackground( Widget.Height, Widget.Width )
if num_msgs > 0
drawNewMailIcon
if @soundfile != nil and @soundfile != ""
Widget.PlayWav @soundfile
end
end
Graphics.DrawString( msg, 10, 10, Widget.Width - 20, Widget.Height - 20 )
Widget.UpdateWidget()
end
if Settings.GetSettingNode( "widgetroot/multithreaded" ) == "Yes"
main_thread.priority = 2
else
main_thread.join
end
end
# Return the subject and the sender in an array if the message
# should be deleted. Otherwise, return nil.
def checkDeletion( header )
do_delete = false
subject = nil
from = nil
header.each do |l|
line = l.chomp
if not do_delete
$delete_regexps.each do |regexp|
if line =~ regexp
do_delete = true
end
end
end
if line =~ /^Subject: (.+)/
subject = $1
end
if line =~ /^From: (.+)/
from = $1
end
end
return ( do_delete ? [ subject, from ] : nil )
end
def drawBackground( height = Widget.Height, width = Widget.Width )
Graphics.DrawImageFromFile( Info.WidgetPath() + "Resources/default.bg.png", 0, 0, width, height )
end
def drawNewMailIcon
Graphics.DrawImage(
$icon,
Settings.GetSettingAttribute( "widgetroot/icon", "x" ).to_i,
Settings.GetSettingAttribute( "widgetroot/icon", "y" ).to_i,
Settings.GetSettingAttribute( "widgetroot/icon", "width" ).to_i,
Settings.GetSettingAttribute( "widgetroot/icon", "height" ).to_i
)
end
def mailIconCorner
x = Settings.GetSettingAttribute( "widgetroot/icon", "x" ).to_i +
Settings.GetSettingAttribute( "widgetroot/icon", "width" ).to_i
y = Settings.GetSettingAttribute( "widgetroot/icon", "y" ).to_i +
Settings.GetSettingAttribute( "widgetroot/icon", "height" ).to_i
return [x, y]
end
def Widget_OnDoubleClick()
$seen = $msg_ids.dup
Widget_Paint()
end
def POPCheck_tick()
Widget_Paint()
end
def Widget_OnKeyPress( key )
$seen = Hash.new
Widget_Paint()
end
# These functions are for handling the "move away on right click" functionality.
def Widget_OnMouseUp( x, y, button )
if button == MouseButton.Right
move( x, y )
end
end
def MoveTimer_Tick()
moveBack
end
# Moves the widget away, based on the click coordinates.
MOVE_POSITIONS = [ 0.5, 0.8, 0.9, 0.96, 1 ]
def move( click_x, click_y )
@old_top = Widget.Top
MOVE_POSITIONS.each do |pos|
Widget.Top = @old_top + ( Widget.Height * pos ).to_i
end
@moved = true
Widget.AddTimer( "MoveTimer", 5000 )
end
def moveBack
Widget.DeleteTimer "MoveTimer"
@moved = false
Widget.Top = @old_top
end
# POP3 SSL support
class Net::POP3S < Net::POP3
def self.default_port
995
end
def do_start( account, password )
s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
sslctx = OpenSSL::SSL::SSLContext.new
s = OpenSSL::SSL::SSLSocket.new(s, sslctx)
s.sync_close = true
s.connect
@socket = Net::SInternetMessageIO.new(s)
@socket.read_timeout = @read_timeout
@socket.debug_output = @debug_output
on_connect
@command = Net::POP3Command.new(@socket)
if apop?
@command.apop account, password
else
@command.auth account, password
end
@started = true
ensure
# Authentication failed, clean up connection.
unless @started
s.close if s and not s.closed?
@socket = nil
@command = nil
end
end
private :do_start
end
class Net::SInternetMessageIO < Net::InternetMessageIO
attr_accessor :debug_output
def initialize(s)
@socket = s
@rbuf = ''
end
end