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.

@@ -0,0 +1,118 @@
1
+ module EventMachine
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.
5
+ 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
16
+ end
17
+
18
+ def send_literal(str)
19
+ @connection.send_literal str, @command
20
+ end
21
+
22
+ public :send_data
23
+ end
24
+
25
+ # This is a method that synchronously converts the command into fragments
26
+ # of string.
27
+ #
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
37
+ end
38
+ sender.put_string CRLF
39
+ end
40
+
41
+ # See Net::IMAP#authenticate
42
+ def send_authentication_data(auth_handler, command)
43
+ when_not_awaiting_continuation do
44
+ waiter = await_continuations do |response|
45
+ begin
46
+ data = auth_handler.process(response.data.text.unpack("m")[0])
47
+ s = [data].pack("m").gsub(/\n/, "")
48
+ send_data(s + CRLF)
49
+ rescue => e
50
+ command.fail e
51
+ end
52
+ end
53
+ command.bothback{ |*args| waiter.stop }
54
+ end
55
+ end
56
+
57
+ def prepare_idle_continuation(command)
58
+ when_not_awaiting_continuation do
59
+ waiter = await_continuations
60
+ command.stopback do
61
+ waiter.stop
62
+ begin
63
+ send_data "DONE\r\n"
64
+ rescue => e
65
+ command.fail e
66
+ end
67
+ end
68
+ end
69
+ end
70
+
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
+
100
+ module LineBuffer
101
+ def post_init
102
+ super
103
+ @line_buffer = ""
104
+ end
105
+
106
+ def send_line_buffered(str)
107
+ @line_buffer += str
108
+ while eol = @line_buffer.index(CRLF)
109
+ to_send = @line_buffer.slice! 0, eol + CRLF.size
110
+ send_data to_send
111
+ end
112
+ end
113
+ end
114
+ include IMAP::CommandSender::LineBuffer
115
+ include IMAP::ContinuationSynchronisation
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,181 @@
1
+ module EventMachine
2
+ module IMAP
3
+ CRLF = "\r\n"
4
+ module Connection
5
+ include EM::Deferrable
6
+ DG.enhance!(self)
7
+
8
+ include IMAP::CommandSender
9
+ include IMAP::ResponseParser
10
+
11
+ # Create a new connection to an IMAP server.
12
+ #
13
+ # @param host, The host name (warning DNS lookups are synchronous)
14
+ # @param port, The port to connect to.
15
+ # @param ssl=false, Whether or not to use TLS.
16
+ #
17
+ # @return Connection, a deferrable that will succeed when the server
18
+ # has replied with OK or PREAUTH, or fail if the
19
+ # connection could not be established, or the
20
+ # first response was BYE.
21
+ #
22
+ def self.connect(host, port, ssl=false)
23
+ conn = EventMachine.connect(host, port, self).tap do |conn|
24
+ conn.start_tls if ssl
25
+ end
26
+ end
27
+
28
+ # Send the command, with the given arguments, to the IMAP server.
29
+ #
30
+ # @param cmd, the name of the command to send (a string)
31
+ # @param *args, the arguments for the command, serialized
32
+ # by Net::IMAP. (FIXME)
33
+ #
34
+ # @return Command, a listener and deferrable that will receive_event
35
+ # with the responses from the IMAP server, and which
36
+ # will succeed with a tagged response from the
37
+ # server, or fail with a tagged error response, or
38
+ # an exception.
39
+ #
40
+ # NOTE: The responses it overhears may be intended
41
+ # for other commands that are running in parallel.
42
+ #
43
+ # Exceptions thrown during serialization will be thrown to the user,
44
+ # exceptions thrown while communicating to the socket will cause the
45
+ # returned command to fail.
46
+ #
47
+ def send_command(cmd, *args)
48
+ Command.new(next_tag!, cmd, args).tap do |command|
49
+ add_to_listener_pool(command)
50
+ listen_for_tagged_response(command)
51
+ listen_for_bye_response(command)
52
+ send_command_object(command)
53
+ end
54
+ end
55
+
56
+ # Create a new listener for responses from the IMAP server.
57
+ #
58
+ # @param &block, a block to which all responses will be passed.
59
+ # @return Listener, an object with a .stop method that you can
60
+ # use to unregister this block.
61
+ #
62
+ # You may also want to listen on the Listener's errback
63
+ # for when problems arise. The Listener's callbacks will
64
+ # be called after you call its stop method.
65
+ #
66
+ def add_response_handler(&block)
67
+ Listener.new(&block).tap do |listener|
68
+ listener.stopback{ listener.succeed }
69
+ 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
+ end
97
+ end
98
+
99
+ # Called when the connection is closed.
100
+ # If there are any listeners left, we fail them.
101
+ # (TODO: Should we actually succeed them if the connection was
102
+ # explicitly closed by us?)
103
+ # TODO: Figure out how to send a useful error...
104
+ def unbind
105
+ @listeners.each{ |listener| listener.fail EOFError.new("Connection to IMAP server was unbound") }
106
+ end
107
+
108
+ def add_to_listener_pool(listener)
109
+ @listeners << listener.bothback{ @listeners.delete listener }
110
+ end
111
+
112
+ # receive_response is a higher-level receive_data provided by
113
+ # EM::IMAP::ResponseParser. Each response is a Net::IMAP response
114
+ # object. (FIXME)
115
+ def receive_response(response)
116
+ @listeners.each{ |listener| listener.receive_event response }
117
+ end
118
+
119
+ # Await the response that marks the completion of this command,
120
+ # and succeed or fail the command as appropriate.
121
+ def listen_for_tagged_response(command)
122
+ command.listen do |response|
123
+ if response.is_a?(Net::IMAP::TaggedResponse) && response.tag == command.tag
124
+ case response.name
125
+ when "NO"
126
+ command.fail Net::IMAP::NoResponseError.new(response.data.text)
127
+ when "BAD"
128
+ command.fail Net::IMAP::BadResponseError.new(response.data.text)
129
+ else
130
+ command.succeed response
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ # If we receive a BYE response from the server, then we're not going
137
+ # to hear any more, so we fail all our listeners.
138
+ def listen_for_bye_response(listener)
139
+ listener.listen do |response|
140
+ if response.is_a?(Net::IMAP::UntaggedResponse) && response.name == "BYE"
141
+ listener.fail Net::IMAP::ByeResponseError.new(response.raw_data)
142
+ end
143
+ end
144
+ end
145
+
146
+ # Provides a next_tag! method to generate unique tags
147
+ # for an IMAP session.
148
+ module TagSequence
149
+ def post_init
150
+ super
151
+ # Copying Net::IMAP
152
+ @tag_prefix = "RUBY"
153
+ @tagno = 0
154
+ end
155
+
156
+ def next_tag!
157
+ @tagno += 1
158
+ "%s%04d" % [@tag_prefix, @tagno]
159
+ end
160
+ end
161
+
162
+ # Intercepts send_data and receive_data and logs them to STDOUT,
163
+ # this should be the last module included.
164
+ module Debug
165
+ def send_data(data)
166
+ puts "C: #{data.inspect}"
167
+ super
168
+ end
169
+
170
+ def receive_data(data)
171
+ puts "S: #{data.inspect}"
172
+ super
173
+ end
174
+ end
175
+ include IMAP::Connection::TagSequence
176
+ def self.debug!
177
+ include IMAP::Connection::Debug
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,85 @@
1
+ module EventMachine
2
+ module IMAP
3
+ # The basic IMAP protocol is an unsynchronised exchange of lines,
4
+ # however under some circumstances it is necessary to synchronise
5
+ # so that the server acknowledges each item sent by the client.
6
+ #
7
+ # For example, this happens during authentication:
8
+ #
9
+ # C: A0001 AUTHENTICATE LOGIN
10
+ # S: +
11
+ # C: USERNAME
12
+ # S: +
13
+ # C: PASSWORD
14
+ # S: A0001 OK authenticated as USERNAME.
15
+ #
16
+ # And during the sending of literals:
17
+ #
18
+ # C: A0002 SELECT {8}
19
+ # S: + continue
20
+ # C: All Mail
21
+ # S: A0002 OK
22
+ #
23
+ # In order to make this work this module allows part of the client
24
+ # to block the outbound link while waiting for the continuation
25
+ # responses that it is expecting.
26
+ #
27
+ module ContinuationSynchronisation
28
+
29
+ def post_init
30
+ super
31
+ @awaiting_continuation = nil
32
+ listen_for_continuation
33
+ end
34
+
35
+ def awaiting_continuation?
36
+ !!@awaiting_continuation
37
+ end
38
+
39
+ # Await further continuation responses from the server, and
40
+ # pass them to the given block.
41
+ #
42
+ # As a side-effect causes when_not_awaiting_continuations to
43
+ # queue further blocks instead of executing them immediately.
44
+ #
45
+ # NOTE: If there's currently a different block awaiting continuation
46
+ # responses, this block will be added to its queue.
47
+ def await_continuations(&block)
48
+ Listener.new(&block).tap do |waiter|
49
+ when_not_awaiting_continuation do
50
+ @awaiting_continuation = waiter.stopback do
51
+ @awaiting_continuation = nil
52
+ waiter.succeed
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # Add a single, permanent listener to the connection that forwards
59
+ # continuation responses onto the currently awaiting block.
60
+ def listen_for_continuation
61
+ add_response_handler do |response|
62
+ if awaiting_continuation? && response.is_a?(Net::IMAP::ContinuationRequest)
63
+ @awaiting_continuation.receive_event response
64
+ end
65
+ end
66
+ end
67
+
68
+ # If nothing is listening for continuations from the server,
69
+ # execute the block immediately.
70
+ #
71
+ # Otherwise add the block to the queue.
72
+ #
73
+ # When we have replied to the server's continuation response,
74
+ # the queue will be emptied in-order.
75
+ #
76
+ def when_not_awaiting_continuation(&block)
77
+ if awaiting_continuation?
78
+ @awaiting_continuation.bothback{ when_not_awaiting_continuation(&block) }
79
+ else
80
+ yield
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,154 @@
1
+ module EventMachine
2
+ module IMAP
3
+ # A Listener is a cancellable subscriber to an event stream, they are used
4
+ # to provide control-flow abstraction throughout em-imap.
5
+ #
6
+ # They can be thought of as a deferrable with two internal phases:
7
+ #
8
+ # deferrable: create |-------------------------------> [succeed/fail]
9
+ # listener: create |---[listening]----> [stop]-----> [succeed/fail]
10
+ #
11
+ # A stopback may call succeed or fail immediately, or after performing
12
+ # necessary cleanup.
13
+ #
14
+ # There are several hooks to which you can subscribe:
15
+ #
16
+ # #listen(&block): Each time .receive_event is called, the block will
17
+ # be called.
18
+ #
19
+ # #stopback(&block): When someone calls .stop on the listener, this block
20
+ # will be called.
21
+ #
22
+ # #callback(&block), #errback(&block), #bothback(&block): Inherited from
23
+ # deferrables (and enhanced by deferrable gratification).
24
+ #
25
+ #
26
+ # And the corresponding methods for sending messages to subscribers:
27
+ #
28
+ # #receive_event(*args): Passed onto blocks registered by listen.
29
+ #
30
+ # #stop(*args): Calls all the stopbacks.
31
+ #
32
+ # #succeed(*args), #fail(*args): Inherited from deferrables.
33
+ #
34
+ #
35
+ # Listeners are defined in such a way that it's most natural to create them
36
+ # from deep within a library, and return them to the original caller via
37
+ # layers of abstraction.
38
+ #
39
+ # To this end, they also have a .transform method which can be used to
40
+ # create a new listener that acts the same as the old listener, but which
41
+ # succeeds with a different return value. The call to .stop is propagated
42
+ # from the new listener to the old, but calls to .receive_event, .succeed
43
+ # and .fail are propagated from the old to the new.
44
+ #
45
+ # This slightly contrived example shows how listeners can be used with three
46
+ # levels of abstraction juxtaposed:
47
+ #
48
+ # def receive_characters
49
+ # Listener.new.tap do |listener|
50
+ #
51
+ # continue = true
52
+ # listener.stopback{ continue = false }
53
+ #
54
+ # EM::next_tick do
55
+ # while continue
56
+ # if key = $stdin.read(1)
57
+ # listener.receive_event key
58
+ # else
59
+ # continue = false
60
+ # listener.fail EOFError.new
61
+ # end
62
+ # end
63
+ # listener.succeed
64
+ # end
65
+ # end
66
+ # end
67
+ #
68
+ # def get_line
69
+ # buffer = ""
70
+ # listener = receive_characters.listen do |key|
71
+ # buffer << key
72
+ # listener.stop if key == "\n"
73
+ # end.transform do
74
+ # buffer
75
+ # end
76
+ # end
77
+ #
78
+ # EM::run do
79
+ # get_line.callback do |line|
80
+ # puts "DONE: #{line}"
81
+ # end.errback do |e|
82
+ # puts [e] + e.backtrace
83
+ # end.bothback do
84
+ # EM::stop
85
+ # end
86
+ # end
87
+ #
88
+ module ListeningDeferrable
89
+ include EM::Deferrable
90
+ DG.enhance!(self)
91
+
92
+ # Register a block to be called when receive_event is called.
93
+ def listen(&block)
94
+ listeners << block
95
+ self
96
+ end
97
+
98
+ # Pass arguments onto any blocks registered with listen.
99
+ def receive_event(*args, &block)
100
+ listeners.each{ |l| l.call *args, &block }
101
+ end
102
+
103
+ # Register a block to be called when the ListeningDeferrable is stopped.
104
+ def stopback(&block)
105
+ stop_deferrable.callback &block
106
+ self
107
+ end
108
+
109
+ # Initiate shutdown.
110
+ def stop(*args, &block)
111
+ stop_deferrable.succeed *args, &block
112
+ end
113
+
114
+ # A re-implementation of DG::Combinators#transform.
115
+ #
116
+ # The returned listener will succeed at the same time as this listener,
117
+ # but the value with which it succeeds will have been transformed using
118
+ # the given block. If this listener fails, the returned listener will
119
+ # also fail with the same arguments.
120
+ #
121
+ # In addition, any events that this listener receives will be forwarded
122
+ # to the new listener, and the stop method of the new listener will also
123
+ # stop the existing listener.
124
+ #
125
+ # NOTE: This does not affect the implementation of bind! which still
126
+ # returns a normal deferrable, not a listener.
127
+ #
128
+ def transform(&block)
129
+ Listener.new.tap do |listener|
130
+ self.callback do |*args|
131
+ listener.succeed block.call(*args)
132
+ end.errback do |*args|
133
+ listener.fail *args
134
+ end.listen do |*args|
135
+ listener.receive_event *args
136
+ end
137
+
138
+ listener.stopback{ self.stop }
139
+ end
140
+ end
141
+
142
+ private
143
+ def listeners; @listeners ||= []; end
144
+ def stop_deferrable; @stop_deferrable ||= DefaultDeferrable.new; end
145
+ end
146
+
147
+ class Listener
148
+ include ListeningDeferrable
149
+ def initialize(&block)
150
+ listen &block if block_given?
151
+ end
152
+ end
153
+ end
154
+ end