Created
August 27, 2010 18:30
-
-
Save bpot/553914 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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