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 +19 -0
- data/README.md +179 -0
- data/lib/em-imap.rb +40 -0
- data/lib/em-imap/authenticators.rb +28 -0
- data/lib/em-imap/client.rb +493 -0
- data/lib/em-imap/command_sender.rb +118 -0
- data/lib/em-imap/connection.rb +181 -0
- data/lib/em-imap/continuation_synchronisation.rb +85 -0
- data/lib/em-imap/listener.rb +154 -0
- data/lib/em-imap/response_parser.rb +50 -0
- metadata +108 -0
@@ -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
|