Skip to content

Instantly share code, notes, and snippets.

@bpot
Created August 27, 2010 18:30
Show Gist options
  • Save bpot/553914 to your computer and use it in GitHub Desktop.
Save bpot/553914 to your computer and use it in GitHub Desktop.
# EM.run {
# iw = ImapWatcher.new(user, pass);
#
# iw.watch! {
# p "sup, yall got new mail"
# }
# }
#
require 'rubygems'
require 'eventmachine'
require 'net/imap'
class ImapWatcher
attr_accessor :state
def initialize(username, password)
@username = username
@password = password
@state = :initial
@mail_count = nil
end
def handle_authentication_error(&block)
@on_authentication_error = block.to_proc
end
def handle_examine_error(&block)
@on_examine_error = block.to_proc
end
def watch!(&on_new_message)
@on_new_message = on_new_message.to_proc
connect
end
def connect
@connection = EM.connect "imap.gmail.com", 993, ImapConnection
#p "IMAP Connection for #{@username}: #{@connection.object_id}"
@connection.on_ready do
change_state(:ready)
end
@connection.on_disconnect do
#p "Reconnecting #{@username}"
connect
end
end
def change_state(new_state)
@state = new_state
on_state_change
end
def on_state_change
case @state
when :ready
authenticate_command = @connection.send_command("LOGIN #{@username} #{@password}")
authenticate_command.callback { change_state(:authenticated) }
authenticate_command.errback { authentication_error }
when :authenticated
# TODO prolly don't want to hardcode mailbox here
authenticate_command = @connection.send_command("EXAMINE Inbox")
authenticate_command.callback {|lines|
# setting initial mail count
@mail_count = parse_mail_count(lines)
change_state(:examining)
}
authenticate_command.errback { examine_error }
when :examining
change_state(:idle)
idle_command = IdleCommand.new(@connection)
idle_command.on_exists { |line|
new_mail_count = parse_mail_count([line])
if new_mail_count > @mail_count
@on_new_message.call
end
@mail_count = new_mail_count
}
idle_command.on_expunge {
# Some message was removed, for the purposes of this class we dont care what it was
@mail_count -= 1
}
end
end
def authentication_error
@on_authentication_error.call if @on_authentication_error
end
def examine_error
@on_examine_error.call if @on_examine_error
end
def parse_mail_count(lines)
count = nil
lines.each do |line|
md = line.match(/(\d+) EXISTS$/)
count = md[1] if md
end
count.to_i
end
end
# heart of the beast
class ImapConnection < EM::Connection
include EM::Protocols::LineText2
def post_init
@state = :unconnected
@buffer = []
@tag_count = 0
end
def connection_completed
start_tls
end
def ssl_handshake_completed
@state = :connected
end
def unbind
#p "#{self.object_id} -- received unbind"
@on_disconnect.call
end
def on_ready(&block)
@on_ready = block.to_proc
end
def on_disconnect(&block)
@on_disconnect = block.to_proc
end
def connection_ready
@state = :ready
@on_ready.call
end
def receive_line(line)
#p "RL (#{self.object_id}): #{line}"
case @state
when :connected
connection_ready if line =~ /OK/
when :ready
when :awaiting_tagged_response
@buffer << line
return unless tagged_response?(line)
if line =~ /OK/
@state = :ready
@deferred_command.set_deferred_status :succeeded, @buffer
@buffer = []
end
if line =~ /BAD|NO/
@state = :ready
@deferred_command.set_deferred_status :failed, @buffer
@buffer = []
end
when :awaiting_idle
if line == "+ idling"
@state = :idling
else
raise "Received unexcepted idle"
end
when :idling
@idling_callback.call(line)
else
raise "OMGz unrecognized state"
end
end
def send_command(command)
raise "OMGz0r not ready" unless @state == :ready
send_data "#{current_tag} #{command}\r\n"
@state = :awaiting_tagged_response
@deferred_command = DeferredImapCommand.new
end
def idle(&callback)
@idling_callback = callback.to_proc
@state = :awaiting_idle
send_data "#{current_tag} IDLE\r\n"
end
def idle_done
send_data "DONE\r\n"
@state = :awaiting_tagged_response
@deferred_command = DeferredImapCommand.new
end
# TODO make tag padded
def current_tag
@tag_count += 1
"EM" + @tag_count.to_s
end
def tagged_response?(line)
!(line =~ /^\* /)
end
end
class DeferredImapCommand
include EM::Deferrable
end
# manages idling
class IdleCommand
def initialize(connection)
@exists_callback = nil
@connection = connection
start_idling
EM.add_periodic_timer(15*60) { restart_idle }
end
def on_exists(&callback)
@exists_callback = callback.to_proc
end
def on_expunge(&callback)
@expunge_callback = callback.to_proc
end
private
def handle_line(line)
if line =~ /EXISTS/ && @exists_callback
@exists_callback.call(line)
elsif line =~ /EXPUNGE/ && @expunge_callback
@expunge_callback.call(line)
end
end
def start_idling
@connection.idle do |line|
handle_line(line)
end
end
def restart_idle
done_command = @connection.idle_done
done_command.callback { start_idling }
done_command.errback { raise "Idle stop failed" }
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment