From: Eric Anderson Date: 2005-04-11T23:59:50+09:00 Subject: Re: accessing gmail account via POP3 --------------090407040906040804060503 Content-Type: text/plain; charset=ISO-8859-1; format=flowed Content-Transfer-Encoding: 7bit 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 --------------090407040906040804060503 Content-Type: text/plain; name="kapmail.kap" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="kapmail.kap" 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 --------------090407040906040804060503--