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