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