initial import
[embrace.git] / lib / embrace / imap.rb
1 # Copyright (c) 2006 Tilman Sauerbeck (tilman at code-monkey de)
2 #
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.
6 #
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.
11 #
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.
15
16 require "ecore"
17 require "ecore_con"
18
19 module Embrace; end
20
21 module Embrace::IMAP
22         class IMAPError < StandardError; end
23         class LoginError < IMAPError; end
24
25         class MailboxStatusEvent < Ecore::Event
26                 attr_reader :name, :count
27
28                 def initialize(name, count)
29                         super()
30
31                         @name, @count = name, count
32                 end
33         end
34
35         class FinishedEvent < Ecore::Event; end
36
37         class Session
38                 attr_reader :connection
39
40                 def initialize(server_info, mboxes)
41                         @server_info = server_info
42
43                         @connection = Ecore::Con::Server.new(2 | 16,
44                                                              @server_info[:host],
45                                                              @server_info[:port])
46                         @buffer = ""
47
48                         @tag_id = 0
49                         @requests = []
50
51                         @mboxes = mboxes.dup
52                         @state = :disconnected
53
54                         @handlers = [
55                                 Ecore::EventHandler.new(Ecore::Con::ServerAddEvent,
56                                                         &method(:on_add)),
57                                 Ecore::EventHandler.new(Ecore::Con::ServerDataEvent,
58                                                         &method(:on_data)),
59                                 Ecore::EventHandler.new(Ecore::Con::ServerDelEvent,
60                                                         &method(:on_del))
61                         ]
62                 end
63
64                 def next_tag
65                         @tag_id = 0 if @tag_id == 0xffff
66
67                         "0x%x" % [@tag_id += 1]
68                 end
69
70                 private
71                 def login(login, password)
72                         @requests << LoginRequest.new(self, login, password).send
73                 end
74
75                 def logout
76                         @requests << LogoutRequest.new(self).send
77                 end
78
79                 def query_status(mailbox)
80                         @requests << StatusRequest.new(self, mailbox).send
81                 end
82
83                 def on_add(ev)
84                         return true unless ev.server == @connection
85
86                         @state = :connected
87
88                         false
89                 end
90
91                 def on_data(ev)
92                         return true unless ev.server == @connection
93
94                         lines = (@buffer + ev.data).split(/\r\n/, -1)
95                         @buffer = lines.pop
96
97                         lines.each { |line| handle_line(line.strip) }
98
99                         false
100                 end
101
102                 def on_del(ev)
103                         return true unless ev.server == @connection
104
105                         @handlers.each { |h| h.delete }
106
107                         @state = :disconnected
108                         @connection.delete
109
110                         FinishedEvent.raise
111
112                         false
113                 end
114
115                 def handle_line(line)
116 if $DEBUG
117                         puts("<- " + line)
118 end
119                         handle_response(Response.deserialize(line))
120
121                         if @state == :connected
122                                 login(@server_info[:login], @server_info[:password])
123                         end
124                 end
125
126                 def handle_response(resp)
127                         unless resp.tagged?
128                                 handle_untagged_response(resp)
129                         else
130                                 req = @requests.find { |r| r.tag == resp.tag }
131                                 handle_tagged_response(resp, req)
132                                 @requests.delete(req)
133                         end
134                 end
135
136                 def handle_tagged_response(resp, req)
137                         return unless req.is_a?(LoginRequest)
138
139                         case resp.status
140                         when :ok
141                                 @state = :logged_in
142
143                                 @mboxes.each { |mb| query_status(mb) }
144                         else
145                                 raise(LoginError, "cannot login - #{resp.data}")
146                         end
147                 end
148
149                 def handle_untagged_response(resp)
150                         return if resp.key != "STATUS"
151
152                         md = resp.data.match(/\"(.+)\" \(UNSEEN (\d+)\)/)
153                         if md.nil?
154                                 raise(IMAPError, "invalid status response - #{resp.data}")
155                         end
156
157                         name, count = md.captures.first, md.captures.last.to_i
158
159                         MailboxStatusEvent.raise(name, count)
160                         @mboxes.delete(name)
161
162                         if @mboxes.empty?
163                                 @status = :done
164                                 logout
165                         end
166                 end
167         end
168
169         class Request
170                 attr_reader :tag
171
172                 def initialize(session)
173                         @session = session
174                         @tag = session.next_tag
175                 end
176
177                 def send
178 if $DEBUG
179                         puts("-> #{@tag} #{serialize}")
180 end
181                         @session.connection << "#{@tag} #{serialize}\r\n"
182
183                         self
184                 end
185         end
186
187         class LoginRequest < Request
188                 def initialize(session, login, password)
189                         super(session)
190
191                         @login = login
192                         @password = password
193                 end
194
195                 def serialize
196                         "LOGIN #{@login} #{@password}"
197                 end
198         end
199
200         class LogoutRequest < Request
201                 def initialize(session)
202                         super
203                 end
204
205                 def serialize
206                         "LOGOUT"
207                 end
208         end
209
210         class StatusRequest < Request
211                 def initialize(session, mailbox)
212                         super(session)
213
214                         @mailbox = mailbox
215                 end
216
217                 def serialize
218                         "STATUS #{@mailbox} (UNSEEN)"
219                 end
220         end
221
222         class Response
223                 class ResponseError < IMAPError; end
224
225                 attr_reader :data
226
227                 def Response.deserialize(line)
228                         case line
229                         when /^\* (\w+) (.*)$/
230                                 UntaggedResponse.new($1, $2)
231                         when /^(0x[[:xdigit:]]+) (OK|NO|BAD) /
232                                 tag = $1
233                                 rest = line[(tag.length + 1)..-1]
234                                 status = $2.downcase.to_sym
235
236                                 TaggedResponse.new(tag, status, rest)
237                         else
238                                 raise(ResponseError, "cannot parse - #{line}")
239                         end
240                 end
241
242                 def initialize(data)
243                         @data = data.to_str.freeze
244                 end
245
246                 def tagged?
247                         false
248                 end
249         end
250
251         class TaggedResponse < Response
252                 attr_reader :tag, :status
253
254                 def initialize(tag, status, data)
255                         super(data)
256
257                         @tag = tag
258                         @status = status
259                 end
260
261                 def tagged?
262                         true
263                 end
264         end
265
266         class UntaggedResponse < Response
267                 attr_reader :key
268
269                 def initialize(key, data)
270                         super(data)
271
272                         @key = key
273                 end
274         end
275 end