1 # Copyright (c) 2006 Tilman Sauerbeck (tilman at code-monkey de)
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of version 2 of the GNU General Public License as
5 # published by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software Foundation,
14 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22 class IMAPError < StandardError; end
23 class LoginError < IMAPError; end
25 class MailboxStatusEvent < Ecore::Event
26 attr_reader :name, :count
28 def initialize(name, count)
31 @name, @count = name, count
35 class FinishedEvent < Ecore::Event; end
38 attr_reader :connection
40 def initialize(server_info, mboxes)
41 @server_info = server_info
43 @connection = Ecore::Con::Server.new(2 | 16,
52 @state = :disconnected
55 Ecore::EventHandler.new(Ecore::Con::ServerAddEvent,
57 Ecore::EventHandler.new(Ecore::Con::ServerDataEvent,
59 Ecore::EventHandler.new(Ecore::Con::ServerDelEvent,
65 @tag_id = 0 if @tag_id == 0xffff
67 "0x%x" % [@tag_id += 1]
71 def login(login, password)
72 @requests << LoginRequest.new(self, login, password).send
76 @requests << LogoutRequest.new(self).send
79 def query_status(mailbox)
80 @requests << StatusRequest.new(self, mailbox).send
84 return true unless ev.server == @connection
92 return true unless ev.server == @connection
94 lines = (@buffer + ev.data).split(/\r\n/, -1)
97 lines.each { |line| handle_line(line.strip) }
103 return true unless ev.server == @connection
105 @handlers.each { |h| h.delete }
107 @state = :disconnected
115 def handle_line(line)
119 handle_response(Response.deserialize(line))
121 if @state == :connected
122 login(@server_info[:login], @server_info[:password])
126 def handle_response(resp)
128 handle_untagged_response(resp)
130 req = @requests.find { |r| r.tag == resp.tag }
131 handle_tagged_response(resp, req)
132 @requests.delete(req)
136 def handle_tagged_response(resp, req)
137 return unless req.is_a?(LoginRequest)
143 @mboxes.each { |mb| query_status(mb) }
145 raise(LoginError, "cannot login - #{resp.data}")
149 def handle_untagged_response(resp)
150 return if resp.key != "STATUS"
152 md = resp.data.match(/\"(.+)\" \(UNSEEN (\d+)\)/)
154 raise(IMAPError, "invalid status response - #{resp.data}")
157 name, count = md.captures.first, md.captures.last.to_i
159 MailboxStatusEvent.raise(name, count)
172 def initialize(session)
174 @tag = session.next_tag
179 puts("-> #{@tag} #{serialize}")
181 @session.connection << "#{@tag} #{serialize}\r\n"
187 class LoginRequest < Request
188 def initialize(session, login, password)
196 "LOGIN #{@login} #{@password}"
200 class LogoutRequest < Request
201 def initialize(session)
210 class StatusRequest < Request
211 def initialize(session, mailbox)
218 "STATUS #{@mailbox} (UNSEEN)"
223 class ResponseError < IMAPError; end
227 def Response.deserialize(line)
229 when /^\* (\w+) (.*)$/
230 UntaggedResponse.new($1, $2)
231 when /^(0x[[:xdigit:]]+) (OK|NO|BAD) /
233 rest = line[(tag.length + 1)..-1]
234 status = $2.downcase.to_sym
236 TaggedResponse.new(tag, status, rest)
238 raise(ResponseError, "cannot parse - #{line}")
243 @data = data.to_str.freeze
251 class TaggedResponse < Response
252 attr_reader :tag, :status
254 def initialize(tag, status, data)
266 class UntaggedResponse < Response
269 def initialize(key, data)