em-imap 0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of em-imap might be problematic. Click here for more details.

data/LICENSE.MIT ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Conrad Irwin <conrad@rapportive.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,179 @@
1
+ An [EventMachine](http://eventmachine.org/) based [IMAP](http://tools.ietf.org/html/rfc3501) client.
2
+
3
+ ## Installation
4
+
5
+ gem install em-imap
6
+
7
+ ## Usage
8
+
9
+ This document tries to introduce concepts of IMAP alongside the facilities of the library that handle them, to give you an idea of how to perform basic IMAP operations. IMAP is more fully explained in [RFC3501](http://tools.ietf.org/html/rfc3501), and the details of the library are of course in the source code.
10
+
11
+ ### Connecting
12
+
13
+ Before you can communicate with an IMAP server, you must first connect to it. There are three connection parameters, the hostname, the port number, and whether to use SSL/TLS. As with every method in EM::IMAP, `EM::IMAP.connect` returns a [deferrable](http://eventmachine.rubyforge.org/docs/DEFERRABLES.html) enhanced by the [deferrable\_gratification](https://github.com/samstokes/deferrable_gratification) library.
14
+
15
+ For example, to connect to Gmail's IMAP server, you can use the following snippet:
16
+
17
+ require 'rubygems'
18
+ require 'em-imap'
19
+
20
+ EM::run do
21
+ client = EM::IMAP.connect('imap.gmail.com', 993, true)
22
+ client.errback do |error|
23
+ puts "Connecting failed: #{error}"
24
+ end.callback do |hello_response|
25
+ puts "Connecting succeeded!"
26
+ end.bothback do
27
+ EM::stop
28
+ end
29
+ end
30
+
31
+ ### Authenticating
32
+
33
+ There are two authentication mechanisms in IMAP, `LOGIN` and `AUTHENTICATE`, exposed as two methods on the EM::IMAP client, `.login(username, password)` and `.authenticate(mechanism, *args)`. Again these methods both return deferrables, and the cleanest way to tie deferrables together is to use the [`.bind!`](http://samstokes.github.com/deferrable_gratification/doc/DeferrableGratification/Combinators.html#bind!-instance_method) method from deferrable\_gratification.
34
+
35
+ Extending our previous example to also log in to Gmail:
36
+
37
+ client = EM::IMAP.connect('imap.gmail.com', 993, true)
38
+ client.bind! do
39
+ client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
40
+ end.callback do
41
+ puts "Connected and logged in!"
42
+ end.errback do |error|
43
+ puts "Connecting or logging in failed: #{error}"
44
+ end
45
+
46
+ The `.authenticate` method is more advanced and uses the same extensible mechanism as [Net::IMAP](http://www.ruby-doc.org/stdlib/libdoc/net/imap/rdoc/classes/Net/IMAP.html). The two mechanisms supported by default are `'LOGIN'` and [`'CRAM-MD5'`](http://www.ietf.org/rfc/rfc2195.txt), other mechanisms are provided by gems like [gmail\_xoauth](https://github.com/nfo/gmail_xoauth).
47
+
48
+ ### Mailbox-level IMAP
49
+
50
+ Once the authentication has completed successfully, you can perform IMAP commands that don't require a currently selected mailbox. For example to get a list of the names of all Gmail mailboxes (including labels):
51
+
52
+ client = EM::IMAP.connect('imap.gmail.com', 993, true)
53
+ client.bind! do
54
+ client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
55
+ end.bind! do
56
+ client.list
57
+ end.callback do |list|
58
+ puts list.map(&:name)
59
+ end.errback do |error|
60
+ puts "Connecting, logging in or listing failed: #{error}"
61
+ end
62
+
63
+ The useful commands available to you at this point are `.list`, `.create(mailbox)`, `.delete(mailbox)`, `.rename(old_mailbox, new_mailbox)`, `.status(mailbox)`. `.select(mailbox)` and `.examine(mailbox)` are discussed in the next section, and `.subscribe(mailbox)`, `.unsubscribe(mailbox)`, `.lsub` and `.append(mailbox, message, flags?, date_time)` are unlikely to be useful to you immediately. For a full list of IMAP commands, and detailed considerations, please refer to [RFC3501](http://tools.ietf.org/html/rfc3501).
64
+
65
+ ### Message-level IMAP
66
+
67
+ In order to do useful things which actual messages, you need to first select a mailbox to interact with. There are two commands for doing this, `.select(mailbox)`, and `.examine(mailbox)`. They are the same except that `.examine` opens a mailbox in read-only mode; so that no changes are made (i.e. performing commands doesn't mark emails as read).
68
+
69
+ For example to search for all emails relevant to em-imap in Gmail:
70
+
71
+ client = EM::IMAP.connect('imap.gmail.com', 993, true)
72
+ client.bind! do
73
+ client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
74
+ end.bind! do
75
+ client.select('[Google Mail]/All Mail')
76
+ end.bind! do
77
+ client.search('ALL', 'SUBJECT', 'em-imap')
78
+ end.callback do |results|
79
+ puts results
80
+ end.errback do |error|
81
+ puts "Something failed: #{error}"
82
+ end
83
+
84
+ Once you have a list of message sequence numbers, as returned by search, you can actually read the emails with `.fetch`:
85
+
86
+ client = EM::IMAP.connect('imap.gmail.com', 993, true)
87
+ client.bind! do
88
+ client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
89
+ end.bind! do
90
+ client.select('[Google Mail]/All Mail')
91
+ end.bind! do
92
+ client.search('ALL', 'SUBJECT', 'em-imap')
93
+ end.bind! do |results|
94
+ client.fetch(results, 'BODY[TEXT]')
95
+ end.callback do |emails|
96
+ puts emails.map{|email| email.attr['BODY[TEXT]'] }
97
+ end.errback do |error|
98
+ puts "Something failed: #{error}"
99
+ end
100
+
101
+ The useful commands available to you at this point are `.search(*args)`, `.expunge`, `.fetch(messages, attributes)`, `.store(messages, name, values)` and `.copy(messages, mailbox)`. If you'd like to work with UIDs instead of sequence numbers, there are UID based alternatives: `.uid_search`, `.uid_fetch`, `.uid_store` and `.uid_copy`. The `.close` command and `.check` command are unlikely to be useful to you immediately.
102
+
103
+ ### Untagged responses
104
+
105
+ IMAP has the notion of untagged responses (aka. unsolicited responses). The idea is that sometimes when you run a command you'd like to be updated on the state of the mailbox with which you are interacting, even though notification isn't always required. To listen for these responses, the deferrables returned by each client method have a `.listen(&block)` method. All responses received by the server, up to and including the response that completes the current command will be passed to your block.
106
+
107
+ For example, we could insert a listener into the above example to find out some interesting numbers:
108
+
109
+ end.bind! do
110
+ client.select('[Google Mail]/All Mail').listen do |response|
111
+ case response.name
112
+ when "EXISTS"
113
+ puts "There are #{response.data} total emails in All Mail"
114
+ when "RECENT"
115
+ puts "There are #{response.data} new emails in All Mail"
116
+ end
117
+ end
118
+ end.bind! do
119
+
120
+ One IMAP command that exists solely to receive such unsolicited responses is IDLE. The IDLE command blocks the connection so that no other commands can use it, so before you can send further commands you must `stop` the IDLE command:
121
+
122
+ idler = client.idle
123
+
124
+ idler.listen do |response|
125
+ if (response.name == "EXISTS" rescue nil)
126
+ puts "Ooh, new emails!"
127
+ idler.stop
128
+ idler.callback do
129
+ # ... process new emails
130
+ end
131
+ end
132
+ end.errback do |e|
133
+ puts "Idler recieved an error: #{e}"
134
+ end
135
+
136
+ ### Concurrency
137
+
138
+ IMAP is an explicitly concurrent protocol: clients MAY send commands without waiting for the previous command to complete, and servers MAY send any untagged response at any time.
139
+
140
+ If you want to receive server responses at any time, you can call `.add_response_handler(&block)` on the client. This returns a deferrable like the IDLE command, on which you can call `stop` to stop receiving responses (which will cause the deferrable to succeed). You should also listen on the `errback` of this deferrable so that you know when the connection is closed:
141
+
142
+ handler = client.add_response_handler do |response|
143
+ puts "Server says: #{response}"
144
+ end.errback do |e|
145
+ puts "Connection closed?: #{e}"
146
+ end
147
+ EM::Timer.new(600){ handler.stop }
148
+
149
+ If you want to send commands without waiting for previous replies, you can also do so. em-imap handles the few cases where this is not permitted (for example, during an IDLE command) by queueing the command until the connection becomes available again. If you do this, bear in mind that any blocks that are listening on the connection may receive responses from multiple commands interleaved.
150
+
151
+ client = EM::Imap.connect('imap.gmail.com', 993, true).callback do
152
+ logger_in = client.login('conrad.irwin@gmail.com', ENV["GMAIL_PASSWORD"])
153
+ selecter = client.select('[Google Mail]/All Mail')
154
+ searcher = client.search('from:conrad@rapportive.com').callback do |results|
155
+ puts results
156
+ end
157
+
158
+ logger_in.errback{ |e| selecter.fail e }
159
+ selecter.errback{ |e| searcher.fail e }
160
+ searcher.errback{ |e| "Something failed: #{e}" }
161
+ end
162
+
163
+ ## TODO
164
+
165
+ em-imap is still very much a work-in-progress, and the API will change as time goes by.
166
+
167
+ Before version 1, at least the following changes should be made:
168
+
169
+ 1. Stop using Net::IMAP in quite so many bizarre ways, probably clearer to copy-paste the code and rename relevant classes (particular NoResponseError..)
170
+ 2. Find a nicer API for some commands (maybe some objects to represent mailboxes, and/or messages?)
171
+ 3. Document argument serialization.
172
+ 4. Support SORT and THREAD.
173
+ 5. Put the in-line documentation into a real format.
174
+
175
+ ## Meta-foo
176
+
177
+ Em-imap is made available under the MIT license, see LICENSE.MIT for details
178
+
179
+ Patches and pull-requests are welcome.
data/lib/em-imap.rb ADDED
@@ -0,0 +1,40 @@
1
+ require 'net/imap'
2
+ require 'set'
3
+
4
+ require 'rubygems'
5
+ require 'eventmachine'
6
+ require 'deferrable_gratification'
7
+
8
+ $:.unshift File.dirname( __FILE__ )
9
+ require 'em-imap/listener'
10
+ require 'em-imap/continuation_synchronisation'
11
+ require 'em-imap/command_sender'
12
+ require 'em-imap/response_parser'
13
+ require 'em-imap/connection'
14
+
15
+ require 'em-imap/authenticators'
16
+ require 'em-imap/client'
17
+ $:.shift
18
+
19
+ module EventMachine
20
+ module IMAP
21
+ # Connect to the specified IMAP server, using ssl if applicable.
22
+ #
23
+ # Returns a deferrable that will succeed or fail based on the
24
+ # success of the connection setup phase.
25
+ #
26
+ def self.connect(host, port, ssl=false)
27
+ Client.new(EventMachine::IMAP::Connection.connect(host, port, ssl))
28
+ end
29
+
30
+ class Command < Listener
31
+ attr_accessor :tag, :cmd, :args
32
+ def initialize(tag, cmd, args=[], &block)
33
+ super(&block)
34
+ self.tag = tag
35
+ self.cmd = cmd
36
+ self.args = args
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ module EventMachine
2
+ # Makes Net::IMAP.add_authenticator accessible through EM::IMAP and instances thereof.
3
+ # Also provides the authenticator method to EM::IMAP::Client to get authenticators
4
+ # for use in the authentication exchange.
5
+ #
6
+ module IMAP
7
+ def self.add_authenticator(klass)
8
+ Net::IMAP.add_authenticator(*args)
9
+ end
10
+
11
+ module Authenticators
12
+ def add_authenticator(*args)
13
+ EventMachine::IMAP.add_authenticator(*args)
14
+ end
15
+
16
+ private
17
+
18
+ def authenticator(type, *args)
19
+ raise ArgumentError, "Unknown auth type - '#{type}'" unless imap_authenticators[type]
20
+ imap_authenticators[type].new(*args)
21
+ end
22
+
23
+ def imap_authenticators
24
+ Net::IMAP.send :class_variable_get, :@@authenticators
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,493 @@
1
+ module EventMachine
2
+ module IMAP
3
+ # TODO: Anything that accepts or returns a mailbox name should have UTF7 support.
4
+ class Client
5
+ include EM::Deferrable
6
+ DG.enhance!(self)
7
+
8
+ include IMAP::Authenticators
9
+
10
+ def initialize(connection)
11
+ @connection = connection.errback{ |e| fail e }.callback{ |response| succeed response }
12
+ end
13
+
14
+ def disconnect
15
+ @connection.close_connection
16
+ end
17
+
18
+ ## 6.1 Client Commands - Any State.
19
+
20
+ # Ask the server which capabilities it supports.
21
+ #
22
+ # Succeeds with an array of capabilities.
23
+ #
24
+ def capability
25
+ one_data_response("CAPABILITY").transform{ |response| response.data }
26
+ end
27
+
28
+ # Actively do nothing.
29
+ #
30
+ # This is useful as a keep-alive, or to persuade the server to send
31
+ # any untagged responses your listeners would like.
32
+ #
33
+ # Succeeds with nil.
34
+ #
35
+ def noop
36
+ tagged_response("NOOP")
37
+ end
38
+
39
+ # Logout and close the connection.
40
+ #
41
+ # This will cause any other listeners or commands that are still active
42
+ # to fail, and render this client unusable.
43
+ #
44
+ def logout
45
+ command = tagged_response("LOGOUT").errback do |e|
46
+ if e.is_a? Net::IMAP::ByeResponseError
47
+ # RFC 3501 says the server MUST send a BYE response and then close the connection.
48
+ disconnect
49
+ command.succeed
50
+ end
51
+ end
52
+ end
53
+
54
+ ## 6.2 Client Commands - Not Authenticated State
55
+
56
+ # This would start tls negotiations, until this is implemented,
57
+ # simply pass true as the first parameter to EM::IMAP.connect.
58
+ #
59
+ def starttls
60
+ raise NotImplementedError
61
+ end
62
+
63
+ # Authenticate using a custom authenticator.
64
+ #
65
+ # By default there are two custom authenticators available:
66
+ #
67
+ # 'LOGIN', username, password
68
+ # 'CRAM-MD5', username, password (see RFC 2195)
69
+ #
70
+ # Though you can add new mechanisms using EM::IMAP.add_authenticator,
71
+ # see for example the gmail_xoauth gem.
72
+ #
73
+ def authenticate(auth_type, *args)
74
+ # Extract these first so that any exceptions can be raised
75
+ # before the command is created.
76
+ auth_type = auth_type.upcase
77
+ auth_handler = authenticator(auth_type, *args)
78
+
79
+ tagged_response('AUTHENTICATE', auth_type).tap do |command|
80
+ @connection.send_authentication_data(auth_handler, command)
81
+ end
82
+ end
83
+
84
+ # Authenticate with a username and password.
85
+ #
86
+ # NOTE: this SHOULD only work over a tls connection.
87
+ #
88
+ # If the password is wrong, the command will fail with a
89
+ # Net::IMAP::NoResponseError.
90
+ #
91
+ def login(username, password)
92
+ tagged_response("LOGIN", username, password)
93
+ end
94
+
95
+ ## 6.3 Client Commands - Authenticated State
96
+
97
+ # Select a mailbox for performing commands against.
98
+ #
99
+ # This will generate untagged responses that you can subscribe to
100
+ # by adding a block to the listener with .listen, for more detail,
101
+ # see RFC 3501, section 6.3.1.
102
+ #
103
+ def select(mailbox)
104
+ tagged_response("SELECT", to_utf7(mailbox))
105
+ end
106
+
107
+ # Select a mailbox for performing read-only commands.
108
+ #
109
+ # This is exactly the same as select, except that no operation may
110
+ # cause a change to the state of the mailbox or its messages.
111
+ #
112
+ def examine(mailbox)
113
+ tagged_response("EXAMINE", to_utf7(mailbox))
114
+ end
115
+
116
+ # Create a new mailbox with the given name.
117
+ #
118
+ def create(mailbox)
119
+ tagged_response("CREATE", to_utf7(mailbox))
120
+ end
121
+
122
+ # Delete the mailbox with this name.
123
+ #
124
+ def delete(mailbox)
125
+ tagged_response("DELETE", to_utf7(mailbox))
126
+ end
127
+
128
+ # Rename the mailbox with this name.
129
+ #
130
+ def rename(oldname, newname)
131
+ tagged_response("RENAME", to_utf7(oldname), to_utf7(newname))
132
+ end
133
+
134
+ # Add this mailbox to the list of subscribed mailboxes.
135
+ #
136
+ def subscribe(mailbox)
137
+ tagged_response("SUBSCRIBE", to_utf7(mailbox))
138
+ end
139
+
140
+ # Remove this mailbox from the list of subscribed mailboxes.
141
+ #
142
+ def unsubscribe(mailbox)
143
+ tagged_response("UNSUBSCRIBE", to_utf7(mailbox))
144
+ end
145
+
146
+ # List all available mailboxes.
147
+ #
148
+ # @param: refname, an optional context in which to list.
149
+ # @param: mailbox, a which mailboxes to return.
150
+ #
151
+ # Succeeds with a list of Net::IMAP::MailboxList structs, each of which has:
152
+ # .name, the name of the mailbox (in UTF8)
153
+ # .delim, the delimeter (normally "/")
154
+ # .attr, A list of attributes, e.g. :Noselect, :Haschildren, :Hasnochildren.
155
+ #
156
+ def list(refname="", pattern="*")
157
+ list_internal("LIST", refname, pattern)
158
+ end
159
+
160
+ # List all subscribed mailboxes.
161
+ #
162
+ # This is the same as list, but restricted to mailboxes that have been subscribed to.
163
+ #
164
+ def lsub(refname, pattern)
165
+ list_internal("LSUB", refname, pattern)
166
+ end
167
+
168
+ # Get the status of a mailbox.
169
+ #
170
+ # This provides similar information to the untagged responses you would
171
+ # get by running SELECT or EXAMINE without doing so.
172
+ #
173
+ # @param mailbox, a mailbox to query
174
+ # @param attrs, a list of attributes to query for (valid values include
175
+ # MESSAGES, RECENT, UIDNEXT, UIDVALIDITY and UNSEEN — RFC3501#6.3.8)
176
+ #
177
+ # Succeeds with a hash of attribute name to value returned by the server.
178
+ #
179
+ def status(mailbox, attrs=['MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'])
180
+ attrs = [attrs] if attrs.is_a?(String)
181
+ one_data_response("STATUS", to_utf7(mailbox), attrs).transform do |response|
182
+ response.data.attr
183
+ end
184
+ end
185
+
186
+ # Add a message to the mailbox.
187
+ #
188
+ # @param mailbox, the mailbox to add to,
189
+ # @param message, the full text (including headers) of the email to add.
190
+ # @param flags, A list of flags to set on the email.
191
+ # @param date_time, The time to be used as the internal date of the email.
192
+ #
193
+ # The tagged response with which this command succeeds contains the UID
194
+ # of the email that was appended.
195
+ #
196
+ def append(mailbox, message, flags=nil, date_time=nil)
197
+ args = [to_utf7(mailbox)]
198
+ args << flags if flags
199
+ args << date_time if date_time
200
+ args << Net::IMAP::Literal.new(message)
201
+ tagged_response("APPEND", *args)
202
+ end
203
+
204
+ # 6.4 Client Commands - Selected State
205
+
206
+ # Checkpoint the current mailbox.
207
+ #
208
+ # This is an implementation-defined operation, when in doubt, NOOP
209
+ # should be used instead.
210
+ #
211
+ def check
212
+ tagged_response("CHECK")
213
+ end
214
+
215
+ # Unselect the current mailbox.
216
+ #
217
+ # As a side-effect, permanently removes any messages that have the
218
+ # \Deleted flag. (Unless the mailbox was selected using the EXAMINE,
219
+ # in which case no side effects occur).
220
+ #
221
+ def close
222
+ tagged_response("CLOSE")
223
+ end
224
+
225
+ # Permanently remove any messages with the \Deleted flag from the current
226
+ # mailbox.
227
+ #
228
+ # Succeeds with a list of message sequence numbers that were deleted.
229
+ #
230
+ # NOTE: If you're planning to EXPUNGE and then SELECT a new mailbox,
231
+ # and you don't care which messages are removed, consider using
232
+ # CLOSE instead.
233
+ #
234
+ def expunge
235
+ multi_data_response("EXPUNGE").transform do |untagged_responses|
236
+ untagged_responses.map(&:data)
237
+ end
238
+ end
239
+
240
+ # Search for messages in the current mailbox.
241
+ #
242
+ # @param *args The arguments to search, these can be strings, arrays or ranges
243
+ # specifying sub-groups of search arguments or sets of messages.
244
+ #
245
+ # If you want to use non-ASCII characters, then the first two
246
+ # arguments should be 'CHARSET', 'UTF-8', though not all servers
247
+ # support this.
248
+ #
249
+ # @succeed A list of message sequence numbers.
250
+ #
251
+ def search(*args)
252
+ search_internal(["SEARCH"], *args)
253
+ end
254
+
255
+ # The same as search, but succeeding with a list of UIDs not sequence numbers.
256
+ #
257
+ def uid_search(*args)
258
+ search_internal(["UID", "SEARCH"], *args)
259
+ end
260
+
261
+ # SORT and THREAD (like SEARCH) from http://tools.ietf.org/search/rfc5256
262
+ #
263
+ def sort(sort_keys, *args)
264
+ raise NotImplementedError
265
+ end
266
+
267
+ def uid_sort(sort_keys, *args)
268
+ raise NotImplementedError
269
+ end
270
+
271
+ def thread(algorithm, *args)
272
+ raise NotImplementedError
273
+ end
274
+
275
+ def uid_thread(algorithm, *args)
276
+ raise NotImplementedError
277
+ end
278
+
279
+ # Get the contents of, or information about, a message.
280
+ #
281
+ # @param seq, a message or sequence of messages (a number, a range or an array of numbers)
282
+ # @param attr, the name of the attribute to fetch, or a list of attributes.
283
+ #
284
+ # Possible attribute names (see RFC 3501 for a full list):
285
+ #
286
+ # ALL: Gets all header information,
287
+ # FULL: Same as ALL with the addition of the BODY,
288
+ # FAST: Same as ALL without the message envelope.
289
+ #
290
+ # BODY: The body
291
+ # BODY[<section>] A particular section of the body
292
+ # BODY[<section>]<<start>,<length>> A substring of a section of the body.
293
+ # BODY.PEEK: The body (but doesn't change the \Recent flag)
294
+ # FLAGS: The flags
295
+ # INTERNALDATE: The internal date
296
+ # UID: The unique identifier
297
+ #
298
+ def fetch(seq, attr="FULL")
299
+ fetch_internal("FETCH", seq, attr)
300
+ end
301
+
302
+ # The same as fetch, but keyed of UIDs instead of sequence numbers.
303
+ #
304
+ def uid_fetch(seq, attr="FULL")
305
+ fetch_internal("UID", "FETCH", seq, attr)
306
+ end
307
+
308
+ # Update the flags on a message.
309
+ #
310
+ # @param seq, a message or sequence of messages (a number, a range, or an array of numbers)
311
+ # @param name, any of FLAGS FLAGS.SILENT, replace the flags
312
+ # +FLAGS, +FLAGS.SILENT, add the following flags
313
+ # -FLAGS, -FLAGS.SILENT, remove the following flags
314
+ # The .SILENT versions suppress the server's responses.
315
+ # @param value, a list of flags (symbols)
316
+ #
317
+ def store(seq, name, value)
318
+ store_internal("STORE", seq, name, value)
319
+ end
320
+
321
+ # The same as store, but keyed off UIDs instead of sequence numbers.
322
+ #
323
+ def uid_store(seq, name, value)
324
+ store_internal("UID", "STORE", seq, name, value)
325
+ end
326
+
327
+ # Copy the specified messages to another mailbox.
328
+ #
329
+ def copy(seq, mailbox)
330
+ tagged_response("COPY", Net::IMAP::MessageSet.new(seq), to_utf7(mailbox))
331
+ end
332
+
333
+ # The same as copy, but keyed off UIDs instead of sequence numbers.
334
+ #
335
+ def uid_copy(seq, mailbox)
336
+ tagged_response("UID", "COPY", Net::IMAP::MessageSet.new(seq), to_utf7(mailbox))
337
+ end
338
+
339
+ # The IDLE command allows you to wait for any untagged responses
340
+ # that give status updates about the contents of a mailbox.
341
+ #
342
+ # Until you call stop on the idler, no further commands can be sent
343
+ # over this connection.
344
+ #
345
+ # idler = connection.idle do |untagged_response|
346
+ # case untagged_response.name
347
+ # #...
348
+ # end
349
+ # end
350
+ #
351
+ # EM.timeout(60) { idler.stop }
352
+ #
353
+ def idle(&block)
354
+ send_command("IDLE").tap do |command|
355
+ @connection.prepare_idle_continuation(command)
356
+ command.listen(&block) if block_given?
357
+ end
358
+ end
359
+
360
+ def add_response_handler(&block)
361
+ @connection.add_response_handler(&block)
362
+ end
363
+
364
+ private
365
+
366
+ # Convert a string to the modified UTF-7 required by IMAP for mailbox naming.
367
+ # See RFC 3501 Section 5.1.3 for more information.
368
+ #
369
+ def to_utf7(text)
370
+ Net::IMAP.encode_utf7(text)
371
+ end
372
+
373
+ # Convert a string from the modified UTF-7 required by IMAP for mailbox naming.
374
+ # See RFC 3501 Section 5.1.3 for more information.
375
+ #
376
+ def to_utf8(text)
377
+ Net::IMAP.decode_utf7(text)
378
+ end
379
+
380
+ # Send a command that should return a deferrable that succeeds with
381
+ # a tagged_response.
382
+ #
383
+ def tagged_response(cmd, *args)
384
+ # We put in an otherwise unnecessary transform to hide the listen
385
+ # method from callers for consistency with other types of responses.
386
+ send_command(cmd, *args)
387
+ end
388
+
389
+ # Send a command that should return a deferrable that succeeds with
390
+ # a single untagged response with the same name as the command.
391
+ #
392
+ def one_data_response(cmd, *args)
393
+ multi_data_response(cmd, *args).transform do |untagged_responses|
394
+ untagged_responses.last
395
+ end
396
+ end
397
+
398
+ # Send a command that should return a deferrable that succeeds with
399
+ # multiple untagged responses with the same name as the command.
400
+ #
401
+ def multi_data_response(cmd, *args)
402
+ collect_untagged_responses(cmd, cmd, *args)
403
+ end
404
+
405
+ # Send a command that should return a deferrable that succeeds with
406
+ # multiple untagged responses with the given name.
407
+ #
408
+ def collect_untagged_responses(name, *command)
409
+ untagged_responses = []
410
+
411
+ send_command(*command).listen do |response|
412
+ if response.is_a?(Net::IMAP::UntaggedResponse) && response.name == name
413
+ untagged_responses << response
414
+
415
+ # If we observe another tagged response completeing, then we can be
416
+ # sure that the previous untagged responses were not relevant to this command.
417
+ elsif response.is_a?(Net::IMAP::TaggedResponse)
418
+ untagged_responses = []
419
+
420
+ end
421
+ end.transform do |tagged_response|
422
+ untagged_responses
423
+ end
424
+ end
425
+
426
+ def send_command(cmd, *args)
427
+ @connection.send_command(cmd, *args)
428
+ end
429
+
430
+ # Extract more useful data from the LIST and LSUB commands, see #list for details.
431
+ def list_internal(cmd, refname, pattern)
432
+ multi_data_response(cmd, to_utf7(refname), to_utf7(pattern)).transform do |untagged_responses|
433
+ untagged_responses.map(&:data).map do |data|
434
+ data.dup.tap do |new_data|
435
+ new_data.name = to_utf8(data.name)
436
+ end
437
+ end
438
+ end
439
+ end
440
+
441
+ # From Net::IMAP
442
+ def fetch_internal(cmd, set, attr)
443
+ case attr
444
+ when String then
445
+ attr = Net::IMAP::RawData.new(attr)
446
+ when Array then
447
+ attr = attr.map { |arg|
448
+ arg.is_a?(String) ? Net::IMAP::RawData.new(arg) : arg
449
+ }
450
+ end
451
+
452
+ set = Net::IMAP::MessageSet.new(set)
453
+
454
+ multi_data_response(cmd, set, attr).transform do |untagged_responses|
455
+ untagged_responses.map(&:data)
456
+ end
457
+ end
458
+
459
+ # Ensure that the flags are symbols, and that the message set is a message set.
460
+ def store_internal(cmd, set, attr, flags)
461
+ flags = flags.map(&:to_sym)
462
+ set = Net::IMAP::MessageSet.new(set)
463
+ collect_untagged_responses('FETCH', cmd, set, attr, flags).transform do |untagged_responses|
464
+ untagged_responses.map(&:data)
465
+ end
466
+ end
467
+
468
+ def search_internal(command, *args)
469
+ command += normalize_search_criteria(args)
470
+ one_data_response(*command).transform{ |untagged_response| untagged_response.data }
471
+ end
472
+
473
+ # Recursively find all the message sets in the arguments and convert them so that
474
+ # Net::IMAP can serialize them.
475
+ def normalize_search_criteria(args)
476
+ args.map do |arg|
477
+ case arg
478
+ when "*", -1, Range
479
+ Net::IMAP::MessageSet.new(arg)
480
+ when Array
481
+ if Array.all?{ |item| item.is_a?(Number) || item.is_a?(Range) }
482
+ Net::IMAP::MessageSet.new(arg)
483
+ else
484
+ normalize_search_criteria(arg)
485
+ end
486
+ else
487
+ arg
488
+ end
489
+ end
490
+ end
491
+ end
492
+ end
493
+ end