em-imap 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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