Adapted to recent Ecore_Con API changes.
[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                         return unless req.is_a?(LoginRequest)
142
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                 end
152
153                 def handle_untagged_response(resp)
154                         return if resp.key != "STATUS"
155
156                         md = resp.data.match(/\"(.+)\" \(UNSEEN (\d+)\)/)
157                         if md.nil?
158                                 raise(IMAPError, "invalid status response - #{resp.data}")
159                         end
160
161                         name, count = md.captures.first, md.captures.last.to_i
162
163                         MailboxStatusEvent.raise(name, count)
164                         @mboxes.delete(name)
165
166                         if @mboxes.empty?
167                                 @status = :done
168                                 logout
169                         end
170                 end
171         end
172
173         class Request
174                 attr_reader :tag
175
176                 def initialize(session)
177                         @session = session
178                         @tag = session.next_tag
179                 end
180
181                 def send
182 if $DEBUG
183                         puts("-> #{@tag} #{serialize}")
184 end
185                         @session.connection << "#{@tag} #{serialize}\r\n"
186
187                         self
188                 end
189         end
190
191         class LoginRequest < Request
192                 def initialize(session, login, password)
193                         super(session)
194
195                         @login = login
196                         @password = password
197                 end
198
199                 def serialize
200                         "LOGIN #{@login} #{@password}"
201                 end
202         end
203
204         class LogoutRequest < Request
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