eventmachine-eventmachine 0.12.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. data/Rakefile +169 -0
  2. data/docs/COPYING +60 -0
  3. data/docs/ChangeLog +183 -0
  4. data/docs/DEFERRABLES +138 -0
  5. data/docs/EPOLL +141 -0
  6. data/docs/GNU +281 -0
  7. data/docs/INSTALL +15 -0
  8. data/docs/KEYBOARD +38 -0
  9. data/docs/LEGAL +25 -0
  10. data/docs/LIGHTWEIGHT_CONCURRENCY +72 -0
  11. data/docs/PURE_RUBY +77 -0
  12. data/docs/README +74 -0
  13. data/docs/RELEASE_NOTES +96 -0
  14. data/docs/SMTP +9 -0
  15. data/docs/SPAWNED_PROCESSES +93 -0
  16. data/docs/TODO +10 -0
  17. data/ext/binder.cpp +126 -0
  18. data/ext/binder.h +48 -0
  19. data/ext/cmain.cpp +530 -0
  20. data/ext/cplusplus.cpp +172 -0
  21. data/ext/ed.cpp +1473 -0
  22. data/ext/ed.h +361 -0
  23. data/ext/em.cpp +1895 -0
  24. data/ext/em.h +170 -0
  25. data/ext/emwin.cpp +300 -0
  26. data/ext/emwin.h +94 -0
  27. data/ext/epoll.cpp +26 -0
  28. data/ext/epoll.h +25 -0
  29. data/ext/eventmachine.h +90 -0
  30. data/ext/eventmachine_cpp.h +94 -0
  31. data/ext/extconf.rb +150 -0
  32. data/ext/files.cpp +94 -0
  33. data/ext/files.h +65 -0
  34. data/ext/kb.cpp +368 -0
  35. data/ext/page.cpp +107 -0
  36. data/ext/page.h +51 -0
  37. data/ext/pipe.cpp +327 -0
  38. data/ext/project.h +119 -0
  39. data/ext/rubymain.cpp +683 -0
  40. data/ext/sigs.cpp +89 -0
  41. data/ext/sigs.h +32 -0
  42. data/ext/ssl.cpp +408 -0
  43. data/ext/ssl.h +86 -0
  44. data/java/src/com/rubyeventmachine/Application.java +196 -0
  45. data/java/src/com/rubyeventmachine/Connection.java +74 -0
  46. data/java/src/com/rubyeventmachine/ConnectionFactory.java +37 -0
  47. data/java/src/com/rubyeventmachine/DefaultConnectionFactory.java +46 -0
  48. data/java/src/com/rubyeventmachine/EmReactor.java +408 -0
  49. data/java/src/com/rubyeventmachine/EmReactorException.java +40 -0
  50. data/java/src/com/rubyeventmachine/EventableChannel.java +57 -0
  51. data/java/src/com/rubyeventmachine/EventableDatagramChannel.java +171 -0
  52. data/java/src/com/rubyeventmachine/EventableSocketChannel.java +244 -0
  53. data/java/src/com/rubyeventmachine/PeriodicTimer.java +38 -0
  54. data/java/src/com/rubyeventmachine/Timer.java +54 -0
  55. data/java/src/com/rubyeventmachine/tests/ApplicationTest.java +108 -0
  56. data/java/src/com/rubyeventmachine/tests/ConnectTest.java +124 -0
  57. data/java/src/com/rubyeventmachine/tests/EMTest.java +80 -0
  58. data/java/src/com/rubyeventmachine/tests/TestDatagrams.java +53 -0
  59. data/java/src/com/rubyeventmachine/tests/TestServers.java +74 -0
  60. data/java/src/com/rubyeventmachine/tests/TestTimers.java +89 -0
  61. data/lib/em/deferrable.rb +208 -0
  62. data/lib/em/eventable.rb +39 -0
  63. data/lib/em/future.rb +62 -0
  64. data/lib/em/messages.rb +66 -0
  65. data/lib/em/processes.rb +68 -0
  66. data/lib/em/spawnable.rb +88 -0
  67. data/lib/em/streamer.rb +112 -0
  68. data/lib/eventmachine.rb +1763 -0
  69. data/lib/eventmachine_version.rb +31 -0
  70. data/lib/evma.rb +32 -0
  71. data/lib/evma/callback.rb +32 -0
  72. data/lib/evma/container.rb +75 -0
  73. data/lib/evma/factory.rb +77 -0
  74. data/lib/evma/protocol.rb +87 -0
  75. data/lib/evma/reactor.rb +48 -0
  76. data/lib/jeventmachine.rb +137 -0
  77. data/lib/pr_eventmachine.rb +1011 -0
  78. data/lib/protocols/buftok.rb +127 -0
  79. data/lib/protocols/header_and_content.rb +129 -0
  80. data/lib/protocols/httpcli2.rb +794 -0
  81. data/lib/protocols/httpclient.rb +270 -0
  82. data/lib/protocols/line_and_text.rb +122 -0
  83. data/lib/protocols/linetext2.rb +163 -0
  84. data/lib/protocols/postgres.rb +261 -0
  85. data/lib/protocols/saslauth.rb +179 -0
  86. data/lib/protocols/smtpclient.rb +308 -0
  87. data/lib/protocols/smtpserver.rb +556 -0
  88. data/lib/protocols/stomp.rb +130 -0
  89. data/lib/protocols/tcptest.rb +57 -0
  90. data/tasks/cpp.rake +77 -0
  91. data/tasks/project.rake +78 -0
  92. data/tasks/tests.rake +192 -0
  93. data/tests/test_attach.rb +66 -0
  94. data/tests/test_basic.rb +231 -0
  95. data/tests/test_defer.rb +47 -0
  96. data/tests/test_epoll.rb +161 -0
  97. data/tests/test_errors.rb +82 -0
  98. data/tests/test_eventables.rb +78 -0
  99. data/tests/test_exc.rb +58 -0
  100. data/tests/test_futures.rb +214 -0
  101. data/tests/test_hc.rb +218 -0
  102. data/tests/test_httpclient.rb +215 -0
  103. data/tests/test_httpclient2.rb +133 -0
  104. data/tests/test_kb.rb +61 -0
  105. data/tests/test_ltp.rb +192 -0
  106. data/tests/test_ltp2.rb +320 -0
  107. data/tests/test_next_tick.rb +102 -0
  108. data/tests/test_processes.rb +56 -0
  109. data/tests/test_pure.rb +129 -0
  110. data/tests/test_running.rb +47 -0
  111. data/tests/test_sasl.rb +74 -0
  112. data/tests/test_send_file.rb +245 -0
  113. data/tests/test_servers.rb +80 -0
  114. data/tests/test_smtpclient.rb +81 -0
  115. data/tests/test_smtpserver.rb +93 -0
  116. data/tests/test_spawn.rb +329 -0
  117. data/tests/test_ssl_args.rb +68 -0
  118. data/tests/test_timers.rb +146 -0
  119. data/tests/test_ud.rb +43 -0
  120. data/tests/testem.rb +31 -0
  121. metadata +197 -0
@@ -0,0 +1,261 @@
1
+ #
2
+ # $Id$
3
+ #
4
+ # Author:: Francis Cianfrocca (gmail: blackhedd)
5
+ # Homepage:: http://rubyeventmachine.com
6
+ # Date:: 15 November 2006
7
+ #
8
+ # See EventMachine and EventMachine::Connection for documentation and
9
+ # usage examples.
10
+ #
11
+ #----------------------------------------------------------------------------
12
+ #
13
+ # Copyright (C) 2006-08 by Francis Cianfrocca. All Rights Reserved.
14
+ # Gmail: blackhedd
15
+ #
16
+ # This program is free software; you can redistribute it and/or modify
17
+ # it under the terms of either: 1) the GNU General Public License
18
+ # as published by the Free Software Foundation; either version 2 of the
19
+ # License, or (at your option) any later version; or 2) Ruby's License.
20
+ #
21
+ # See the file COPYING for complete licensing information.
22
+ #
23
+ #---------------------------------------------------------------------------
24
+ #
25
+ #
26
+ #
27
+
28
+
29
+ =begin
30
+ PROVISIONAL IMPLEMENTATION of an evented Postgres client.
31
+ This implements version 3 of the Postgres wire protocol, which will work
32
+ with any Postgres version from roughly 7.4 onward.
33
+
34
+ Until this code is judged ready for prime time, you have to access it by
35
+ explicitly requiring protocols/postgres.
36
+
37
+ Objective: we want to access Postgres databases without requiring threads.
38
+ Until now this has been a problem because the Postgres client implementations
39
+ have all made use of blocking I/O calls, which is incompatible with a
40
+ thread-free evented model.
41
+
42
+ But rather than re-implement the Postgres Wire3 protocol, we're taking advantage
43
+ of the existing postgres-pr library, which was originally written by Michael
44
+ Neumann but (at this writing) appears to be no longer maintained. Still, it's
45
+ in basically a production-ready state, and the wire protocol isn't that complicated
46
+ anyway.
47
+
48
+ We need to monkeypatch StringIO because it lacks the #readbytes method needed
49
+ by postgres-pr.
50
+
51
+ We're tucking in a bunch of require statements that may not be present in garden-variety
52
+ EM installations. Until we find a good way to only require these if a program
53
+ requires postgres, this file will need to be required explicitly.
54
+
55
+ The StringIO monkeypatch is lifted verbatim from the standard library readbytes.rb,
56
+ which adds method #readbytes directly to class IO. But StringIO is not a subclass of IO.
57
+
58
+ We cloned the handling of postgres messages from lib/postgres-pr/connection.rb
59
+ in the postgres-pr library, and modified it for event-handling.
60
+
61
+ TODO: The password handling in dispatch_conn_message is totally incomplete.
62
+
63
+
64
+ We return Deferrables from the user-level operations surfaced by this interface.
65
+ Experimentally, we're using the pattern of always returning a boolean value as the
66
+ first argument of a deferrable callback to indicate success or failure. This is
67
+ instead of the traditional pattern of calling Deferrable#succeed or #fail, and
68
+ requiring the user to define both a callback and an errback function.
69
+
70
+ Sample code:
71
+ require 'eventmachine'
72
+ require 'protocols/postgres' # provisionally needed
73
+
74
+ EM.run {
75
+ db = EM.connect_unix_domain( "/tmp/.s.PGSQL.5432", EM::P::Postgres3 )
76
+ db.connect( dbname, username, psw ).callback do |status|
77
+ if status
78
+ db.query( "select * from some_table" ).callback do |status, result, errors|
79
+ if status
80
+ result.rows.each do |row|
81
+ p row
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ }
88
+
89
+
90
+
91
+ =end
92
+
93
+
94
+ require 'readbytes'
95
+ require 'postgres-pr/message'
96
+ require 'postgres-pr/connection'
97
+ require 'stringio'
98
+
99
+ include PostgresPR
100
+
101
+ class StringIO
102
+ # Reads exactly +n+ bytes.
103
+ #
104
+ # If the data read is nil an EOFError is raised.
105
+ #
106
+ # If the data read is too short a TruncatedDataError is raised and the read
107
+ # data is obtainable via its #data method.
108
+ def readbytes(n)
109
+ str = read(n)
110
+ if str == nil
111
+ raise EOFError, "End of file reached"
112
+ end
113
+ if str.size < n
114
+ raise TruncatedDataError.new("data truncated", str)
115
+ end
116
+ str
117
+ end
118
+ end
119
+
120
+
121
+ module EventMachine; module Protocols; class Postgres3 < EventMachine::Connection
122
+
123
+
124
+ def initialize
125
+ @data = ""
126
+ @params = {}
127
+ end
128
+
129
+ def connect db, user, psw=nil
130
+ d = EM::DefaultDeferrable.new
131
+ d.timeout 15
132
+
133
+ if @pending_query || @pending_conn
134
+ d.succeed false, "Operation already in progress"
135
+ else
136
+ @pending_conn = d
137
+ prms = {"user"=>user, "database"=>db}
138
+ @user = user
139
+ if psw
140
+ @password = psw
141
+ #prms["password"] = psw
142
+ end
143
+ send_data PostgresPR::StartupMessage.new( 3 << 16, prms ).dump
144
+ end
145
+
146
+ d
147
+ end
148
+
149
+ def query sql
150
+ d = EM::DefaultDeferrable.new
151
+ d.timeout 15
152
+
153
+ if @pending_query || @pending_conn
154
+ d.succeed false, "Operation already in progress"
155
+ else
156
+ @r = PostgresPR::Connection::Result.new
157
+ @e = []
158
+ @pending_query = d
159
+ send_data PostgresPR::Query.dump(sql)
160
+ end
161
+
162
+ d
163
+ end
164
+
165
+
166
+ def receive_data data
167
+ @data << data
168
+ while @data.length >= 5
169
+ pktlen = @data[1...5].unpack("N").first
170
+ if @data.length >= (1 + pktlen)
171
+ pkt = @data.slice!(0...(1+pktlen))
172
+ m = StringIO.open( pkt, "r" ) {|io| PostgresPR::Message.read( io ) }
173
+ if @pending_conn
174
+ dispatch_conn_message m
175
+ elsif @pending_query
176
+ dispatch_query_message m
177
+ else
178
+ raise "Unexpected message from database"
179
+ end
180
+ else
181
+ break # very important, break out of the while
182
+ end
183
+ end
184
+ end
185
+
186
+
187
+ def unbind
188
+ if o = (@pending_query || @pending_conn)
189
+ o.succeed false, "lost connection"
190
+ end
191
+ end
192
+
193
+ # Cloned and modified from the postgres-pr.
194
+ def dispatch_conn_message msg
195
+ case msg
196
+ when AuthentificationClearTextPassword
197
+ raise ArgumentError, "no password specified" if @password.nil?
198
+ send_data PasswordMessage.new(@password).dump
199
+
200
+ when AuthentificationCryptPassword
201
+ raise ArgumentError, "no password specified" if @password.nil?
202
+ send_data PasswordMessage.new(@password.crypt(msg.salt)).dump
203
+
204
+ when AuthentificationMD5Password
205
+ raise ArgumentError, "no password specified" if @password.nil?
206
+ require 'digest/md5'
207
+
208
+ m = Digest::MD5.hexdigest(@password + @user)
209
+ m = Digest::MD5.hexdigest(m + msg.salt)
210
+ m = 'md5' + m
211
+ send_data PasswordMessage.new(m).dump
212
+
213
+ when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential
214
+ raise "unsupported authentification"
215
+
216
+ when AuthentificationOk
217
+ when ErrorResponse
218
+ raise msg.field_values.join("\t")
219
+ when NoticeResponse
220
+ @notice_processor.call(msg) if @notice_processor
221
+ when ParameterStatus
222
+ @params[msg.key] = msg.value
223
+ when BackendKeyData
224
+ # TODO
225
+ #p msg
226
+ when ReadyForQuery
227
+ # TODO: use transaction status
228
+ pc,@pending_conn = @pending_conn,nil
229
+ pc.succeed true
230
+ else
231
+ raise "unhandled message type"
232
+ end
233
+ end
234
+
235
+ # Cloned and modified from the postgres-pr.
236
+ def dispatch_query_message msg
237
+ case msg
238
+ when DataRow
239
+ @r.rows << msg.columns
240
+ when CommandComplete
241
+ @r.cmd_tag = msg.cmd_tag
242
+ when ReadyForQuery
243
+ pq,@pending_query = @pending_query,nil
244
+ pq.succeed true, @r, @e
245
+ when RowDescription
246
+ @r.fields = msg.fields
247
+ when CopyInResponse
248
+ when CopyOutResponse
249
+ when EmptyQueryResponse
250
+ when ErrorResponse
251
+ # TODO
252
+ @e << msg
253
+ when NoticeResponse
254
+ @notice_processor.call(msg) if @notice_processor
255
+ else
256
+ # TODO
257
+ end
258
+ end
259
+
260
+ end; end; end
261
+
@@ -0,0 +1,179 @@
1
+ # $Id$
2
+ #
3
+ # Author:: Francis Cianfrocca (gmail: blackhedd)
4
+ # Homepage:: http://rubyeventmachine.com
5
+ # Date:: 15 November 2006
6
+ #
7
+ # See EventMachine and EventMachine::Connection for documentation and
8
+ # usage examples.
9
+ #
10
+ #----------------------------------------------------------------------------
11
+ #
12
+ # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
13
+ # Gmail: blackhedd
14
+ #
15
+ # This program is free software; you can redistribute it and/or modify
16
+ # it under the terms of either: 1) the GNU General Public License
17
+ # as published by the Free Software Foundation; either version 2 of the
18
+ # License, or (at your option) any later version; or 2) Ruby's License.
19
+ #
20
+ # See the file COPYING for complete licensing information.
21
+ #
22
+ #---------------------------------------------------------------------------
23
+ #
24
+ #
25
+ #
26
+
27
+
28
+
29
+ module EventMachine
30
+ module Protocols
31
+
32
+ # Implements SASL authd.
33
+ # This is a very, very simple protocol that mimics the one used
34
+ # by saslauthd and pwcheck, two outboard daemons included in the
35
+ # standard SASL library distro.
36
+ # The only thing this is really suitable for is SASL PLAIN
37
+ # (user+password) authentication, but the SASL libs that are
38
+ # linked into standard servers (like imapd and sendmail) implement
39
+ # the other ones.
40
+ #
41
+ # SASL-auth is intended for reasonably fast operation inside a
42
+ # single machine, so it has no transport-security (although there
43
+ # have been multi-machine extensions incorporating transport-layer
44
+ # encryption).
45
+ #
46
+ # The standard saslauthd module generally runs privileged and does
47
+ # its work by referring to the system-account files.
48
+ #
49
+ # This feature was added to EventMachine to enable the development
50
+ # of custom authentication/authorization engines for standard servers.
51
+ #
52
+ # To use SASLauth, include it in a class that subclasses EM::Connection,
53
+ # and reimplement the validate method.
54
+ #
55
+ # The typical way to incorporate this module into an authentication
56
+ # daemon would be to set it as the handler for a UNIX-domain socket.
57
+ # The code might look like this:
58
+ #
59
+ # EM.start_unix_domain_server( "/var/run/saslauthd/mux", MyHandler )
60
+ # File.chmod( 0777, "/var/run/saslauthd/mux")
61
+ #
62
+ # The chmod is probably needed to ensure that unprivileged clients can
63
+ # access the UNIX-domain socket.
64
+ #
65
+ # It's also a very good idea to drop superuser privileges (if any), after
66
+ # the UNIX-domain socket has been opened.
67
+ #--
68
+ # Implementation details: assume the client can send us pipelined requests,
69
+ # and that the client will close the connection.
70
+ #
71
+ # The client sends us four values, each encoded as a two-byte length field in
72
+ # network order followed by the specified number of octets.
73
+ # The fields specify the username, password, service name (such as imap),
74
+ # and the "realm" name. We send back the barest minimum reply, a single
75
+ # field also encoded as a two-octet length in network order, followed by
76
+ # either "NO" or "OK" - simplicity itself.
77
+ #
78
+ # We enforce a maximum field size just as a sanity check.
79
+ # We do NOT automatically time out the connection.
80
+ #
81
+ # The code we use to parse out the values is ugly and probably slow.
82
+ # Improvements welcome.
83
+ #
84
+ module SASLauth
85
+
86
+ MaxFieldSize = 128*1024
87
+ def post_init
88
+ super
89
+ @sasl_data = ""
90
+ @sasl_values = []
91
+ end
92
+
93
+ def receive_data data
94
+ @sasl_data << data
95
+ while @sasl_data.length >= 2
96
+ len = (@sasl_data[0,2].unpack("n")).first
97
+ raise "SASL Max Field Length exceeded" if len > MaxFieldSize
98
+ if @sasl_data.length >= (len + 2)
99
+ @sasl_values << @sasl_data[2,len]
100
+ @sasl_data.slice!(0...(2+len))
101
+ if @sasl_values.length == 4
102
+ send_data( validate(*@sasl_values) ? "\0\002OK" : "\0\002NO" )
103
+ @sasl_values.clear
104
+ end
105
+ else
106
+ break
107
+ end
108
+ end
109
+ end
110
+
111
+ def validate username, psw, sysname, realm
112
+ p username
113
+ p psw
114
+ p sysname
115
+ p realm
116
+ true
117
+ end
118
+ end
119
+
120
+ # Implements the SASL authd client protocol.
121
+ # This is a very, very simple protocol that mimics the one used
122
+ # by saslauthd and pwcheck, two outboard daemons included in the
123
+ # standard SASL library distro.
124
+ # The only thing this is really suitable for is SASL PLAIN
125
+ # (user+password) authentication, but the SASL libs that are
126
+ # linked into standard servers (like imapd and sendmail) implement
127
+ # the other ones.
128
+ #
129
+ # You can use this module directly as a handler for EM Connections,
130
+ # or include it in a module or handler class of your own.
131
+ #
132
+ # First connect to a SASL server (it's probably a TCP server, or more
133
+ # likely a Unix-domain socket). Then call the #validate? method,
134
+ # passing at least a username and a password. #validate? returns
135
+ # a Deferrable which will either succeed or fail, depending
136
+ # on the status of the authentication operation.
137
+ #
138
+ module SASLauthclient
139
+ MaxFieldSize = 128*1024
140
+
141
+ def validate? username, psw, sysname=nil, realm=nil
142
+
143
+ str = [username, psw, sysname, realm].map {|m|
144
+ [(m || "").length, (m || "")]
145
+ }.flatten.pack( "nA*" * 4 )
146
+ send_data str
147
+
148
+ d = EM::DefaultDeferrable.new
149
+ @queries.unshift d
150
+ d
151
+ end
152
+
153
+ def post_init
154
+ @sasl_data = ""
155
+ @queries = []
156
+ end
157
+
158
+ def receive_data data
159
+ @sasl_data << data
160
+
161
+ while @sasl_data.length > 2
162
+ len = (@sasl_data[0,2].unpack("n")).first
163
+ raise "SASL Max Field Length exceeded" if len > MaxFieldSize
164
+ if @sasl_data.length >= (len + 2)
165
+ val = @sasl_data[2,len]
166
+ @sasl_data.slice!(0...(2+len))
167
+ q = @queries.pop
168
+ (val == "NO") ? q.fail : q.succeed
169
+ else
170
+ break
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ end
177
+ end
178
+
179
+
@@ -0,0 +1,308 @@
1
+ # $Id$
2
+ #
3
+ # Author:: Francis Cianfrocca (gmail: blackhedd)
4
+ # Homepage:: http://rubyeventmachine.com
5
+ # Date:: 16 July 2006
6
+ #
7
+ # See EventMachine and EventMachine::Connection for documentation and
8
+ # usage examples.
9
+ #
10
+ #----------------------------------------------------------------------------
11
+ #
12
+ # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
13
+ # Gmail: blackhedd
14
+ #
15
+ # This program is free software; you can redistribute it and/or modify
16
+ # it under the terms of either: 1) the GNU General Public License
17
+ # as published by the Free Software Foundation; either version 2 of the
18
+ # License, or (at your option) any later version; or 2) Ruby's License.
19
+ #
20
+ # See the file COPYING for complete licensing information.
21
+ #
22
+ #---------------------------------------------------------------------------
23
+ #
24
+ #
25
+
26
+
27
+ #require 'base64'
28
+ require 'ostruct'
29
+
30
+ module EventMachine
31
+ module Protocols
32
+
33
+
34
+ class SmtpClient < Connection
35
+ include EventMachine::Deferrable
36
+ include EventMachine::Protocols::LineText2
37
+
38
+ # This is the external entry point.
39
+ #
40
+ # The argument is a hash containing these values:
41
+ # :host => a string containing the IP address or host name of the SMTP server to connect to.
42
+ # :port => optional, defaults to 25.
43
+ # :domain => required String. This is passed as the argument to the EHLO command.
44
+ # :starttls => optional. If it evaluates true, then the client will initiate STARTTLS with
45
+ # the server, and abort the connection if the negotiation doesn't succeed.
46
+ # TODO, need to be able to pass certificate parameters with this option.
47
+ # :auth => optional hash of auth parameters. If not given, then no auth will be attempted.
48
+ # (In that case, the connection will be aborted if the server requires auth.)
49
+ # Specify the hash value :type to determine the auth type, along with additional parameters
50
+ # depending on the type.
51
+ # Currently only :type => :plain is supported. Pass additional parameters :username (String),
52
+ # and :password (either a String or a Proc that will be called at auth-time).
53
+ # Example: :auth => {:type=>:plain, :username=>"mickey@disney.com", :password=>"mouse"}
54
+ # :from => required String. Specifies the sender of the message. Will be passed as the argument
55
+ # to the MAIL FROM. Do NOT enclose the argument in angle-bracket (<>) characters.
56
+ # The connection will abort if the server rejects the value.
57
+ # :to => required String or Array of Strings. The recipient(s) of the message. Do NOT enclose
58
+ # any of the values in angle-brackets (<>) characters. It's NOT a fatal error if one or more
59
+ # recipients are rejected by the server. (Of course, if ALL of them are, the server will most
60
+ # likely trigger an error when we try to send data.) An array of codes containing the status
61
+ # of each requested recipient is available after the call completes. TODO, we should define
62
+ # an overridable stub that will be called on rejection of a recipient or a sender, giving
63
+ # user code the chance to try again or abort the connection.
64
+ # :header => Required hash of values to be transmitted in the header of the message. The hash
65
+ # keys are the names of the headers (do NOT append a trailing colon), and the values are strings
66
+ # containing the header values. TODO, support Arrays of header values, which would cause us to
67
+ # send that specific header line more than once.
68
+ # Example: :header => {"Subject" => "Bogus", "CC" => "myboss@example.com"}
69
+ # :body => Optional string, defaults blank. This will be passed as the body of the email message.
70
+ # TODO, this needs to be significantly beefed up. As currently written, this requires the caller
71
+ # to properly format the input into CRLF-delimited lines of 7-bit characters in the standard
72
+ # SMTP transmission format. We need to be able to automatically convert binary data, and add
73
+ # correct line-breaks to text data. I think the :body parameter should remain as it is, and we
74
+ # should add a :content parameter that contains autoconversions and/or conversion parameters.
75
+ # Then we can check if either :body or :content is present and do the right thing.
76
+ # :verbose => Optional. If true, will cause a lot of information (including the server-side of the
77
+ # conversation) to be dumped to $>.
78
+ #
79
+ def self.send args={}
80
+ args[:port] ||= 25
81
+ args[:body] ||= ""
82
+
83
+ =begin
84
+ (I don't think it's possible for EM#connect to throw an exception under normal
85
+ circumstances, so this original code is stubbed out. A connect-failure will result
86
+ in the #unbind method being called without calling #connection_completed.)
87
+ begin
88
+ EventMachine.connect( args[:host], args[:port], self) {|c|
89
+ # According to the EM docs, we will get here AFTER post_init is called.
90
+ c.args = args
91
+ c.set_comm_inactivity_timeout 60
92
+ }
93
+ rescue
94
+ # We'll get here on a connect error. This code mimics the effect
95
+ # of a call to invoke_internal_error. Would be great to DRY this up.
96
+ # (Actually, it may be that we never get here, if EM#connect catches
97
+ # its errors internally.)
98
+ d = EM::DefaultDeferrable.new
99
+ d.set_deferred_status(:failed, {:error=>[:connect, 500, "unable to connect to server"]})
100
+ d
101
+ end
102
+ =end
103
+ EventMachine.connect( args[:host], args[:port], self) {|c|
104
+ # According to the EM docs, we will get here AFTER post_init is called.
105
+ c.args = args
106
+ c.set_comm_inactivity_timeout 60
107
+ }
108
+
109
+ end
110
+
111
+ attr_writer :args
112
+
113
+ def post_init
114
+ @return_values = OpenStruct.new
115
+ @return_values.start_time = Time.now
116
+ end
117
+
118
+ def connection_completed
119
+ @responder = :receive_signon
120
+ @msg = []
121
+ end
122
+
123
+ # We can get here in a variety of ways, all of them being failures unless
124
+ # the @succeeded flag is set. If a protocol success was recorded, then don't
125
+ # set a deferred success because the caller will already have done it
126
+ # (no need to wait until the connection closes to invoke the callbacks).
127
+ #
128
+ def unbind
129
+ unless @succeeded
130
+ @return_values.elapsed_time = Time.now - @return_values.start_time
131
+ @return_values.responder = @responder
132
+ @return_values.code = @code
133
+ @return_values.message = @msg
134
+ set_deferred_status(:failed, @return_values)
135
+ end
136
+ end
137
+
138
+ def receive_line ln
139
+ $>.puts ln if @args[:verbose]
140
+ @range = ln[0...1].to_i
141
+ @code = ln[0...3].to_i
142
+ @msg << ln[4..-1]
143
+ unless ln[3...4] == '-'
144
+ $>.puts @responder if @args[:verbose]
145
+ send @responder
146
+ @msg.clear
147
+ end
148
+ end
149
+
150
+ # We encountered an error from the server and will close the connection.
151
+ # Use the error and message the server returned.
152
+ #
153
+ def invoke_error
154
+ @return_values.elapsed_time = Time.now - @return_values.start_time
155
+ @return_values.responder = @responder
156
+ @return_values.code = @code
157
+ @return_values.message = @msg
158
+ set_deferred_status :failed, @return_values
159
+ send_data "QUIT\r\n"
160
+ close_connection_after_writing
161
+ end
162
+
163
+ # We encountered an error on our side of the protocol and will close the connection.
164
+ # Use an extra-protocol error code (900) and use the message from the caller.
165
+ #
166
+ def invoke_internal_error msg = "???"
167
+ @return_values.elapsed_time = Time.now - @return_values.start_time
168
+ @return_values.responder = @responder
169
+ @return_values.code = 900
170
+ @return_values.message = msg
171
+ set_deferred_status :failed, @return_values
172
+ send_data "QUIT\r\n"
173
+ close_connection_after_writing
174
+ end
175
+
176
+ def receive_signon
177
+ return invoke_error unless @range == 2
178
+ send_data "EHLO #{@args[:domain]}\r\n"
179
+ @responder = :receive_ehlo_response
180
+ end
181
+
182
+ def receive_ehlo_response
183
+ return invoke_error unless @range == 2
184
+ @server_caps = @msg
185
+ invoke_starttls
186
+ end
187
+
188
+ def invoke_starttls
189
+ if @args[:starttls]
190
+ # It would be more sociable to first ask if @server_caps contains
191
+ # the string "STARTTLS" before we invoke it, but hey, life's too short.
192
+ send_data "STARTTLS\r\n"
193
+ @responder = :receive_starttls_response
194
+ else
195
+ invoke_auth
196
+ end
197
+ end
198
+ def receive_starttls_response
199
+ return invoke_error unless @range == 2
200
+ start_tls
201
+ invoke_auth
202
+ end
203
+
204
+
205
+
206
+ # Perform an authentication. If the caller didn't request one, then fall through
207
+ # to the mail-from state.
208
+ def invoke_auth
209
+ if @args[:auth]
210
+ if @args[:auth][:type] == :plain
211
+ psw = @args[:auth][:password]
212
+ if psw.respond_to?(:call)
213
+ psw = psw.call
214
+ end
215
+ #str = Base64::encode64("\0#{@args[:auth][:username]}\0#{psw}").chomp
216
+ str = ["\0#{@args[:auth][:username]}\0#{psw}"].pack("m").chomp
217
+ send_data "AUTH PLAIN #{str}\r\n"
218
+ @responder = :receive_auth_response
219
+ else
220
+ return invoke_internal_error("unsupported auth type")
221
+ end
222
+ else
223
+ invoke_mail_from
224
+ end
225
+ end
226
+ def receive_auth_response
227
+ return invoke_error unless @range == 2
228
+ invoke_mail_from
229
+ end
230
+
231
+ def invoke_mail_from
232
+ send_data "MAIL FROM: <#{@args[:from]}>\r\n"
233
+ @responder = :receive_mail_from_response
234
+ end
235
+ def receive_mail_from_response
236
+ return invoke_error unless @range == 2
237
+ invoke_rcpt_to
238
+ end
239
+
240
+ def invoke_rcpt_to
241
+ @rcpt_responses ||= []
242
+ l = @rcpt_responses.length
243
+ to = @args[:to].is_a?(Array) ? @args[:to] : [@args[:to].to_s]
244
+ if l < to.length
245
+ send_data "RCPT TO: <#{to[l]}>\r\n"
246
+ @responder = :receive_rcpt_to_response
247
+ else
248
+ e = @rcpt_responses.select {|rr| rr.last == 2}
249
+ if e and e.length > 0
250
+ invoke_data
251
+ else
252
+ invoke_error
253
+ end
254
+ end
255
+ end
256
+ def receive_rcpt_to_response
257
+ @rcpt_responses << [@code, @msg, @range]
258
+ invoke_rcpt_to
259
+ end
260
+
261
+ def invoke_data
262
+ send_data "DATA\r\n"
263
+ @responder = :receive_data_response
264
+ end
265
+ def receive_data_response
266
+ return invoke_error unless @range == 3
267
+
268
+ # The data to send can be given either in @args[:content] (an array or string of raw data
269
+ # which MUST be in correct SMTP body format, including a trailing dot line), or a header and
270
+ # body given in @args[:header] and @args[:body].
271
+ #
272
+ if @args[:content]
273
+ send_data @args[:content].to_s
274
+ else
275
+ # The header can be a hash or an array.
276
+ if @args[:header].is_a?(Hash)
277
+ (@args[:header] || {}).each {|k,v| send_data "#{k}: #{v}\r\n" }
278
+ else
279
+ send_data @args[:header].to_s
280
+ end
281
+ send_data "\r\n"
282
+
283
+ if @args[:body].is_a?(Array)
284
+ @args[:body].each {|e| send_data e}
285
+ else
286
+ send_data @args[:body].to_s
287
+ end
288
+
289
+ send_data "\r\n.\r\n"
290
+ end
291
+
292
+ @responder = :receive_message_response
293
+ end
294
+ def receive_message_response
295
+ return invoke_error unless @range == 2
296
+ send_data "QUIT\r\n"
297
+ close_connection_after_writing
298
+ @succeeded = true
299
+ @return_values.elapsed_time = Time.now - @return_values.start_time
300
+ @return_values.responder = @responder
301
+ @return_values.code = @code
302
+ @return_values.message = @msg
303
+ set_deferred_status :succeeded, @return_values
304
+ end
305
+ end
306
+ end
307
+ end
308
+