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.
- 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
|