em-imap 0.1.1 → 0.2

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/README.md CHANGED
@@ -10,7 +10,7 @@ This document tries to introduce concepts of IMAP alongside the facilities of th
10
10
 
11
11
  ### Connecting
12
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.
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::Client#connect` returns a [deferrable](http://eventmachine.rubyforge.org/docs/DEFERRABLES.html) enhanced by the [deferrable\_gratification](https://github.com/samstokes/deferrable_gratification) library.
14
14
 
15
15
  For example, to connect to Gmail's IMAP server, you can use the following snippet:
16
16
 
@@ -18,8 +18,8 @@ For example, to connect to Gmail's IMAP server, you can use the following snippe
18
18
  require 'em-imap'
19
19
 
20
20
  EM::run do
21
- client = EM::IMAP.connect('imap.gmail.com', 993, true)
22
- client.errback do |error|
21
+ client = EM::IMAP.new('imap.gmail.com', 993, true)
22
+ client.connect.errback do |error|
23
23
  puts "Connecting failed: #{error}"
24
24
  end.callback do |hello_response|
25
25
  puts "Connecting succeeded!"
@@ -34,8 +34,8 @@ There are two authentication mechanisms in IMAP, `LOGIN` and `AUTHENTICATE`, exp
34
34
 
35
35
  Extending our previous example to also log in to Gmail:
36
36
 
37
- client = EM::IMAP.connect('imap.gmail.com', 993, true)
38
- client.bind! do
37
+ client = EM::IMAP.new('imap.gmail.com', 993, true)
38
+ client.connect.bind! do
39
39
  client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
40
40
  end.callback do
41
41
  puts "Connected and logged in!"
@@ -49,8 +49,8 @@ The `.authenticate` method is more advanced and uses the same extensible mechani
49
49
 
50
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
51
 
52
- client = EM::IMAP.connect('imap.gmail.com', 993, true)
53
- client.bind! do
52
+ client = EM::IMAP.new('imap.gmail.com', 993, true)
53
+ client.connect.bind! do
54
54
  client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
55
55
  end.bind! do
56
56
  client.list
@@ -68,8 +68,8 @@ In order to do useful things which actual messages, you need to first select a m
68
68
 
69
69
  For example to search for all emails relevant to em-imap in Gmail:
70
70
 
71
- client = EM::IMAP.connect('imap.gmail.com', 993, true)
72
- client.bind! do
71
+ client = EM::IMAP.new('imap.gmail.com', 993, true)
72
+ client.connect.bind! do
73
73
  client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
74
74
  end.bind! do
75
75
  client.select('[Google Mail]/All Mail')
@@ -83,8 +83,8 @@ For example to search for all emails relevant to em-imap in Gmail:
83
83
 
84
84
  Once you have a list of message sequence numbers, as returned by search, you can actually read the emails with `.fetch`:
85
85
 
86
- client = EM::IMAP.connect('imap.gmail.com', 993, true)
87
- client.bind! do
86
+ client = EM::IMAP.new('imap.gmail.com', 993, true)
87
+ client.connect.bind! do
88
88
  client.login("conrad.irwin@gmail.com", ENV["GMAIL_PASSWORD"])
89
89
  end.bind! do
90
90
  client.select('[Google Mail]/All Mail')
@@ -148,7 +148,8 @@ If you want to receive server responses at any time, you can call `.add_response
148
148
 
149
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
150
 
151
- client = EM::Imap.connect('imap.gmail.com', 993, true).callback do
151
+ client = EM::Imap.new('imap.gmail.com', 993, true)
152
+ client.connect.callback do
152
153
  logger_in = client.login('conrad.irwin@gmail.com', ENV["GMAIL_PASSWORD"])
153
154
  selecter = client.select('[Google Mail]/All Mail')
154
155
  searcher = client.search('from:conrad@rapportive.com').callback do |results|
@@ -172,6 +173,10 @@ Before version 1, at least the following changes should be made:
172
173
  4. Support SORT and THREAD.
173
174
  5. Put the in-line documentation into a real format.
174
175
 
176
+ ### Breaking Changes
177
+
178
+ Between Version 0.1(.x) and 0.2, the connection setup API changed. Previously you would call `EM::IMAP.connect`, now that is broken into two steps: `EM::IMAP.new` and `EM::IMAP::Client#connect` as documented above. This makes it less likely people will write `client = connect.bind!` by accident, and allows you to bind to the `errback` of the connection as a whole should you wish to.
179
+
175
180
  ## Meta-foo
176
181
 
177
182
  Em-imap is made available under the MIT license, see LICENSE.MIT for details
@@ -7,8 +7,16 @@ module EventMachine
7
7
 
8
8
  include IMAP::Authenticators
9
9
 
10
- def initialize(connection)
11
- @connection = connection.errback{ |e| fail e }.callback{ |response| succeed response }
10
+ def initialize(host, port, usessl=false)
11
+ @connect_args=[host, port, usessl]
12
+ end
13
+
14
+ def connect
15
+ @connection = EM::IMAP::Connection.connect(*@connect_args)
16
+ @connection.errback{ |e| fail e }.
17
+ callback{ |*args| succeed *args }
18
+
19
+ @connection.hello_listener
12
20
  end
13
21
 
14
22
  def disconnect
@@ -1,44 +1,96 @@
1
1
  module EventMachine
2
2
  module IMAP
3
- # Provides a send_command_object method that serializes command objects
4
- # and uses send_data on them. This is the ugly sister to ResponseParser.
3
+ # Used to send commands, and various other pieces of data, to the IMAP
4
+ # server as they are needed. Plugs in the ContinuationSynchronisation module
5
+ # so that the outgoing channel is free of racey-behaviour.
5
6
  module CommandSender
6
- # Ugly hack to get at the Net::IMAP string formatting routines.
7
- # (FIXME: Extract into its own module and rewrite)
8
- class FakeNetIMAP < Net::IMAP
9
- def initialize(command, imap_connection)
10
- @command = command
11
- @connection = imap_connection
12
- end
13
-
14
- def put_string(str)
15
- @connection.send_string str, @command
7
+ # Send a command to the IMAP server.
8
+ #
9
+ # @param command, The command to send.
10
+ #
11
+ # This method has two phases, the first of which is to convert your
12
+ # command into tokens for sending over the network, and the second is to
13
+ # actually send those fragments.
14
+ #
15
+ # If the conversion fails, a Net::IMAP::DataFormatError will be raised
16
+ # which you should handle synchronously. If the sending fails, then the
17
+ # command will be failed asynchronously.
18
+ #
19
+ def send_command_object(command)
20
+ Formatter.format(command) do |to_send|
21
+ if to_send.is_a? Formatter::Literal
22
+ send_literal to_send.str, command
23
+ else
24
+ send_string to_send, command
25
+ end
16
26
  end
27
+ end
17
28
 
18
- def send_literal(str)
19
- @connection.send_literal str, @command
29
+ # Send some normal (binary/string) data to the server.
30
+ #
31
+ # @param str, the data to send
32
+ # @param command, the command for which the data is being sent.
33
+ #
34
+ # This uses the LineBuffer, and fails the command if the network
35
+ # connection has died for some reason.
36
+ #
37
+ def send_string(str, command)
38
+ when_not_awaiting_continuation do
39
+ begin
40
+ send_line_buffered str
41
+ rescue => e
42
+ command.fail e
43
+ end
20
44
  end
21
-
22
- public :send_data
23
45
  end
24
46
 
25
- # This is a method that synchronously converts the command into fragments
26
- # of string.
47
+ # Send an IMAP literal to the server.
27
48
  #
28
- # If you pass something that cannot be serialized, an exception will be raised.
29
- # If however, something fails at the socket level, the command will be failed.
30
- def send_command_object(command)
31
- sender = FakeNetIMAP.new(command, self)
32
-
33
- sender.put_string "#{command.tag} #{command.cmd}"
34
- command.args.each do |arg|
35
- sender.put_string " "
36
- sender.send_data arg
49
+ # @param literal, the string to send.
50
+ # @param command, the command associated with this string.
51
+ #
52
+ # Sending literals is a somewhat complicated process:
53
+ #
54
+ # Step 1. Client tells the server how big the literal will be.
55
+ # (and at the same time shows the server the contents of the command so
56
+ # far)
57
+ # Step 2. The server either accepts (with a ContinuationResponse) or
58
+ # rejects (with a BadResponse) the continuation based on the size of the
59
+ # literal, and the validity of the line so far.
60
+ # Step 3. The client sends the literal, followed by a linefeed, and then
61
+ # continues with sending the rest of the command.
62
+ #
63
+ def send_literal(literal, command)
64
+ when_not_awaiting_continuation do
65
+ begin
66
+ send_line_buffered "{" + literal.size.to_s + "}" + CRLF
67
+ rescue => e
68
+ command.fail e
69
+ end
70
+ waiter = await_continuations do
71
+ begin
72
+ send_data literal
73
+ rescue => e
74
+ command.fail e
75
+ end
76
+ waiter.stop
77
+ end
78
+ command.errback{ waiter.stop }
37
79
  end
38
- sender.put_string CRLF
39
80
  end
40
81
 
41
- # See Net::IMAP#authenticate
82
+ # Pass a challenge/response between the server and the auth_handler.
83
+ #
84
+ # @param auth_handler, an authorization handler.
85
+ # @param command, the associated AUTHORIZE command.
86
+ #
87
+ # This can be called several times in one authorization handshake
88
+ # depending on how many messages the server wishes to see from the
89
+ # auth_handler.
90
+ #
91
+ # If the auth_handler raises an exception, or the network connection dies
92
+ # for some reason, the command will be failed.
93
+ #
42
94
  def send_authentication_data(auth_handler, command)
43
95
  when_not_awaiting_continuation do
44
96
  waiter = await_continuations do |response|
@@ -54,6 +106,14 @@ module EventMachine
54
106
  end
55
107
  end
56
108
 
109
+ # Register a stopback on the IDLE command that sends the DONE
110
+ # continuation that the server is waiting for.
111
+ #
112
+ # @param command, The IDLE command.
113
+ #
114
+ # This blocks the outgoing connection until the IDLE command is stopped,
115
+ # as required by RFC 2177.
116
+ #
57
117
  def prepare_idle_continuation(command)
58
118
  when_not_awaiting_continuation do
59
119
  waiter = await_continuations
@@ -68,35 +128,12 @@ module EventMachine
68
128
  end
69
129
  end
70
130
 
71
- def send_string(str, command)
72
- when_not_awaiting_continuation do
73
- begin
74
- send_line_buffered str
75
- rescue => e
76
- command.fail e
77
- end
78
- end
79
- end
80
-
81
- def send_literal(literal, command)
82
- when_not_awaiting_continuation do
83
- begin
84
- send_line_buffered "{" + literal.size.to_s + "}" + CRLF
85
- rescue => e
86
- command.fail e
87
- end
88
- waiter = await_continuations do
89
- begin
90
- send_data literal
91
- rescue => e
92
- command.fail e
93
- end
94
- waiter.stop
95
- end
96
- command.errback{ waiter.stop }
97
- end
98
- end
99
131
 
132
+ # Buffers out-going string sending by-line.
133
+ #
134
+ # This is safe to do for IMAP because the client always ends transmission
135
+ # on a CRLF (for awaiting continuation requests, and for ending commands)
136
+ #
100
137
  module LineBuffer
101
138
  def post_init
102
139
  super
@@ -25,6 +25,38 @@ module EventMachine
25
25
  end
26
26
  end
27
27
 
28
+ def post_init
29
+ @listeners = []
30
+ super
31
+ listen_for_failure
32
+ listen_for_greeting
33
+ end
34
+
35
+ # This listens for the IMAP connection to have been set up. This should
36
+ # be shortly after the TCP connection is available, once we've received
37
+ # a greeting from the server.
38
+ def listen_for_greeting
39
+ add_to_listener_pool(hello_listener)
40
+ hello_listener.listen do |response|
41
+ # TODO: Is this the right condition? I think it can be one of several
42
+ # possible answers depending on how trusted the connection is, but probably
43
+ # not *anything* except BYE.
44
+ if response.is_a?(Net::IMAP::UntaggedResponse) && response.name != "BYE"
45
+ hello_listener.succeed response
46
+ else
47
+ hello_listener.fail Net::IMAP::ResponseParseError.new(response.raw_data)
48
+ end
49
+ end.errback do |e|
50
+ hello_listener.fail e
51
+ end
52
+ end
53
+
54
+ # Returns a Listener that is active during connection setup, and which is succeeded
55
+ # or failed as soon as we've received a greeting from the server.
56
+ def hello_listener
57
+ @hello_listener ||= Listener.new.errback{ |e| fail e }.bothback{ hello_listener.stop }
58
+ end
59
+
28
60
  # Send the command, with the given arguments, to the IMAP server.
29
61
  #
30
62
  # @param cmd, the name of the command to send (a string)
@@ -48,7 +80,6 @@ module EventMachine
48
80
  Command.new(next_tag!, cmd, args).tap do |command|
49
81
  add_to_listener_pool(command)
50
82
  listen_for_tagged_response(command)
51
- listen_for_bye_response(command)
52
83
  send_command_object(command)
53
84
  end
54
85
  end
@@ -67,49 +98,9 @@ module EventMachine
67
98
  Listener.new(&block).tap do |listener|
68
99
  listener.stopback{ listener.succeed }
69
100
  add_to_listener_pool(listener)
70
- listen_for_bye_response(listener)
71
- end
72
- end
73
-
74
- def post_init
75
- @listeners = Set.new
76
- super
77
- listen_for_greeting
78
- end
79
-
80
- # Listen for the first response from the server and succeed or fail
81
- # the connection deferrable.
82
- def listen_for_greeting
83
- hello_listener = add_response_handler do |response|
84
- hello_listener.stop
85
- if response.is_a?(Net::IMAP::UntaggedResponse)
86
- if response.name == "BYE"
87
- fail Net::IMAP::ByeResponseError.new(response.raw_data)
88
- else
89
- succeed response
90
- end
91
- else
92
- fail Net::IMAP::ResponseParseError.new(response.raw_data)
93
- end
94
- end.errback do |e|
95
- fail e
96
101
  end
97
102
  end
98
103
 
99
- # Called when the connection is closed.
100
- # TODO: Figure out how to send a useful error...
101
- def unbind
102
- fail_all EOFError.new("Connection to IMAP server was unbound"), true
103
- end
104
-
105
- def fail_all(error, closed=false)
106
- # NOTE: Take a shallow clone of the listeners here so that we get guaranteed
107
- # behaviour. We want to fail any listeners that may be added by the errbacks
108
- # of other listeners.
109
- @listeners.clone.each{ |listener| listener.fail error } while @listeners.size > 0
110
- close_connection unless closed
111
- end
112
-
113
104
  def add_to_listener_pool(listener)
114
105
  @listeners << listener.bothback{ @listeners.delete listener }
115
106
  end
@@ -140,12 +131,31 @@ module EventMachine
140
131
  end
141
132
  end
142
133
 
143
- # If we receive a BYE response from the server, then we're not going
144
- # to hear any more, so we fail all our listeners.
145
- def listen_for_bye_response(listener)
146
- listener.listen do |response|
134
+ # Called when the connection is closed.
135
+ # TODO: Figure out how to send a useful error...
136
+ def unbind
137
+ @unbound = true
138
+ fail EOFError.new("Connection to IMAP server was unbound")
139
+ end
140
+
141
+ # Attach life-long listeners on various conditions that we want to treat as connection
142
+ # errors. When such an error occurs, we want to fail all the currently pending commands
143
+ # so that the user of the library doesn't have to subscribe to more than one stream
144
+ # of errors.
145
+ def listen_for_failure
146
+ errback do |error|
147
+ # NOTE: Take a shallow clone of the listeners here so that we get guaranteed
148
+ # behaviour. We want to fail any listeners that may be added by the errbacks
149
+ # of other listeners.
150
+ @listeners.clone.each{ |listener| listener.fail error } while @listeners.size > 0
151
+ close_connection unless @unbound
152
+ end
153
+
154
+ # If we receive a BYE response from the server, then we're not going
155
+ # to hear any more, so we fail all our listeners.
156
+ add_response_handler do |response|
147
157
  if response.is_a?(Net::IMAP::UntaggedResponse) && response.name == "BYE"
148
- listener.fail Net::IMAP::ByeResponseError.new(response.raw_data)
158
+ fail Net::IMAP::ByeResponseError.new(response.raw_data)
149
159
  end
150
160
  end
151
161
  end
@@ -63,7 +63,7 @@ module EventMachine
63
63
  if awaiting_continuation?
64
64
  @awaiting_continuation.receive_event response
65
65
  else
66
- fail_all Net::IMAP::ResponseError.new("Unexpected continuation response from server")
66
+ fail Net::IMAP::ResponseError.new("Unexpected continuation response from server")
67
67
  end
68
68
  end
69
69
  end
@@ -0,0 +1,124 @@
1
+ module EventMachine
2
+ module IMAP
3
+ class Formatter
4
+
5
+ # A placeholder so that the command sender knows to treat literal strings specially
6
+ class Literal < Struct.new(:str); end
7
+
8
+ # Format the data to be sent into strings and literals, and call the block
9
+ # for each token to be sent.
10
+ #
11
+ # @param data The data to format,
12
+ # @param &block The callback, which will be called with a number of strings and
13
+ # EM::IMAP::Formatter::Literal instances.
14
+ #
15
+ # NOTE: The block is responsible for handling any network-level concerns, such
16
+ # as sending literals only with permission.
17
+ #
18
+ def self.format(data, &block)
19
+ new(&block).send_data(data)
20
+ end
21
+
22
+ def initialize(&block)
23
+ @block = block
24
+ end
25
+
26
+ def put_string(str)
27
+ @block.call str
28
+ end
29
+
30
+ def send_literal(str)
31
+ @block.call Literal.new(str)
32
+ end
33
+
34
+ # The remainder of the code in this file is directly from Net::IMAP.
35
+ # Copyright (C) 2000 Shugo Maeda <shugo@ruby-lang.org>
36
+ def send_data(data)
37
+ case data
38
+ when nil
39
+ put_string("NIL")
40
+ when String
41
+ send_string_data(data)
42
+ when Integer
43
+ send_number_data(data)
44
+ when Array
45
+ send_list_data(data)
46
+ when Time
47
+ send_time_data(data)
48
+ when Symbol
49
+ send_symbol_data(data)
50
+ when EM::IMAP::Command
51
+ send_command(data)
52
+ else
53
+ data.send_data(self)
54
+ end
55
+ end
56
+
57
+ def send_command(cmd)
58
+ put_string cmd.tag
59
+ put_string " "
60
+ put_string cmd.cmd
61
+ cmd.args.each do |i|
62
+ put_string " "
63
+ send_data(i)
64
+ end
65
+ put_string "\r\n"
66
+ end
67
+
68
+ def send_string_data(str)
69
+ case str
70
+ when ""
71
+ put_string('""')
72
+ when /[\x80-\xff\r\n]/n
73
+ # literal
74
+ send_literal(str)
75
+ when /[(){ \x00-\x1f\x7f%*"\\]/n
76
+ # quoted string
77
+ send_quoted_string(str)
78
+ else
79
+ put_string(str)
80
+ end
81
+ end
82
+
83
+ def send_quoted_string(str)
84
+ put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
85
+ end
86
+
87
+ def send_number_data(num)
88
+ if num < 0 || num >= 4294967296
89
+ raise Net::IMAP::DataFormatError, num.to_s
90
+ end
91
+ put_string(num.to_s)
92
+ end
93
+
94
+ def send_list_data(list)
95
+ put_string("(")
96
+ first = true
97
+ list.each do |i|
98
+ if first
99
+ first = false
100
+ else
101
+ put_string(" ")
102
+ end
103
+ send_data(i)
104
+ end
105
+ put_string(")")
106
+ end
107
+
108
+ DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
109
+
110
+ def send_time_data(time)
111
+ t = time.dup.gmtime
112
+ s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
113
+ t.day, DATE_MONTH[t.month - 1], t.year,
114
+ t.hour, t.min, t.sec)
115
+ put_string(s)
116
+ end
117
+
118
+ def send_symbol_data(symbol)
119
+ put_string("\\" + symbol.to_s)
120
+ end
121
+
122
+ end
123
+ end
124
+ end
@@ -92,7 +92,6 @@ module EventMachine
92
92
  # Register a block to be called when receive_event is called.
93
93
  def listen(&block)
94
94
  listeners << block
95
- stopback{ |*args| listeners.delete block }
96
95
  self
97
96
  end
98
97
 
@@ -39,15 +39,12 @@ module EventMachine
39
39
  # Callback used by receive data.
40
40
  def receive_response(response); end
41
41
 
42
- # Callback used if something goes wrong.
43
- def fail_all(error); end
44
-
45
42
  private
46
43
 
47
44
  def parse(line)
48
45
  @parser.parse(line)
49
46
  rescue Net::IMAP::ResponseParseError => e
50
- fail_all e
47
+ fail e
51
48
  end
52
49
  end
53
50
  end
data/lib/em-imap.rb CHANGED
@@ -8,6 +8,7 @@ require 'deferrable_gratification'
8
8
  $:.unshift File.dirname( __FILE__ )
9
9
  require 'em-imap/listener'
10
10
  require 'em-imap/continuation_synchronisation'
11
+ require 'em-imap/formatter'
11
12
  require 'em-imap/command_sender'
12
13
  require 'em-imap/response_parser'
13
14
  require 'em-imap/connection'
@@ -27,6 +28,10 @@ module EventMachine
27
28
  Client.new(EventMachine::IMAP::Connection.connect(host, port, ssl))
28
29
  end
29
30
 
31
+ def self.new(host, port, ssl=false)
32
+ Client.new(host, port, ssl)
33
+ end
34
+
30
35
  class Command < Listener
31
36
  attr_accessor :tag, :cmd, :args
32
37
  def initialize(tag, cmd, args=[], &block)
metadata CHANGED
@@ -1,13 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-imap
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 15
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 1
10
- version: 0.1.1
8
+ - 2
9
+ version: "0.2"
11
10
  platform: ruby
12
11
  authors:
13
12
  - Conrad Irwin
@@ -72,6 +71,7 @@ extra_rdoc_files: []
72
71
  files:
73
72
  - lib/em-imap.rb
74
73
  - lib/em-imap/listener.rb
74
+ - lib/em-imap/formatter.rb
75
75
  - lib/em-imap/command_sender.rb
76
76
  - lib/em-imap/authenticators.rb
77
77
  - lib/em-imap/connection.rb