DEFINITIONS
This source file includes following functions.
1 =begin
2
3 = net/smtp.rb
4
5 Copyright (c) 1999-2002 Yukihiro Matsumoto
6
7 written & maintained by Minero Aoki <aamine@loveruby.net>
8
9 This program is free software. You can re-distribute and/or
10 modify this program under the same terms as Ruby itself,
11 Ruby Distribute License or GNU General Public License.
12
13 NOTE: You can find Japanese version of this document in
14 the doc/net directory of the standard ruby interpreter package.
15
16 $Id: smtp.rb,v 1.52 2002/07/29 06:14:10 matz Exp $
17
18 == What is This Module?
19
20 This module provides your program the functions to send internet
21 mail via SMTP, Simple Mail Transfer Protocol. For details of
22 SMTP itself, refer [RFC2821] ((<URL:http:
23
24 == What This Module is NOT?
25
26 This module does NOT provide the functions to compose internet
27 mail. You must create it by yourself. For details of internet mail
28 format, see [RFC2822] ((<URL:http:
29
30 == Examples
31
32 === Sending Mail
33
34 You must open connection to SMTP server before sending mails.
35 First argument is the address of SMTP server, and second argument
36 is port number. Using SMTP.start with block is the most simple way
37 to do it. SMTP Connection is closed automatically after block is
38 executed.
39
40 require 'net/smtp'
41 Net::SMTP.start( 'your.smtp.server', 25 ) {|smtp|
42 # use smtp object only in this block
43 }
44
45 Replace 'your.smtp.server' by your SMTP server. Normally
46 your system manager or internet provider is supplying a server
47 for you.
48
49 Then you can send mail.
50
51 require 'net/smtp'
52
53 Net::SMTP.start( 'your.smtp.server', 25 ) {|smtp|
54 smtp.send_mail <<EndOfMail, 'your@mail.address', 'to@some.domain'
55 From: Your Name <your@mail.address>
56 To: Dest Address <to@some.domain>
57 Subject: test mail
58 Date: Sat, 23 Jun 2001 16:26:43 +0900
59 Message-Id: <unique.message.id.string@some.domain>
60
61 This is test mail.
62 EndOfMail
63 }
64
65 === Closing Session
66
67 You MUST close SMTP session after sending mails, by calling #finish
68 method. You can also use block form of SMTP.start/SMTP#start, which
69 closes session automatically. I strongly recommend later one. It is
70 more beautiful and simple.
71
72 # using SMTP#finish
73 smtp = Net::SMTP.start( 'your.smtp.server', 25 )
74 smtp.send_mail mail_string, 'from@address', 'to@address'
75 smtp.finish
76
77 # using block form of SMTP.start
78 Net::SMTP.start( 'your.smtp.server', 25 ) {|smtp|
79 smtp.send_mail mail_string, 'from@address', 'to@address'
80 }
81
82 === Sending Mails from Any Sources
83
84 In an example above I sent mail from String (here document literal).
85 SMTP#send_mail accepts any objects which has "each" method
86 like File and Array.
87
88 require 'net/smtp'
89 Net::SMTP.start( 'your.smtp.server', 25 ) {|smtp|
90 File.open( 'Mail/draft/1' ) {|f|
91 smtp.send_mail f, 'your@mail.address', 'to@some.domain'
92 }
93 }
94
95 === HELO domain
96
97 In almost all situation, you must designate the third argument
98 of SMTP.start/SMTP#start. It is the domain name which you are on
99 (the host to send mail from). It is called "HELO domain".
100 SMTP server will judge if he/she should send or reject
101 the SMTP session by inspecting HELO domain.
102
103 Net::SMTP.start( 'your.smtp.server', 25,
104 'mail.from.domain' ) {|smtp|
105
106
107 == class Net::SMTP
108
109 === Class Methods
110
111 : new( address, port = 25 )
112 creates a new Net::SMTP object.
113
114 : start( address, port = 25, helo_domain = 'localhost.localdomain', account = nil, password = nil, authtype = nil )
115 : start( address, port = 25, helo_domain = 'localhost.localdomain', account = nil, password = nil, authtype = nil ) {|smtp| .... }
116 is equal to
117 Net::SMTP.new(address,port).start(helo_domain,account,password,authtype)
118
119 # example
120 Net::SMTP.start( 'your.smtp.server' ) {
121 smtp.send_mail mail_string, 'from@mail.address', 'dest@mail.address'
122 }
123
124 === Instance Methods
125
126 : start( helo_domain = <local host name>, account = nil, password = nil, authtype = nil )
127 : start( helo_domain = <local host name>, account = nil, password = nil, authtype = nil ) {|smtp| .... }
128 opens TCP connection and starts SMTP session.
129 HELO_DOMAIN is a domain that you'll dispatch mails from.
130 If protocol had been started, raises IOError.
131
132 When this methods is called with block, give a SMTP object to block and
133 close session after block call finished.
134
135 If both of account and password are given, is trying to get
136 authentication by using AUTH command. :plain or :cram_md5 is
137 allowed for AUTHTYPE.
138
139 : active?
140 true if SMTP session is started.
141
142 : address
143 the address to connect
144
145 : port
146 the port number to connect
147
148 : open_timeout
149 : open_timeout=(n)
150 seconds to wait until connection is opened.
151 If SMTP object cannot open a conection in this seconds,
152 it raises TimeoutError exception.
153
154 : read_timeout
155 : read_timeout=(n)
156 seconds to wait until reading one block (by one read(1) call).
157 If SMTP object cannot open a conection in this seconds,
158 it raises TimeoutError exception.
159
160 : finish
161 finishes SMTP session.
162 If SMTP session had not started, raises an IOError.
163
164 : send_mail( mailsrc, from_addr, *to_addrs )
165 This method sends MAILSRC as mail. A SMTP object read strings
166 from MAILSRC by calling "each" iterator, with converting them
167 into CRLF ("\r\n") terminated string when write.
168
169 FROM_ADDR must be a String, representing source mail address.
170 TO_ADDRS must be Strings or an Array of Strings, representing
171 destination mail addresses.
172
173 # example
174 Net::SMTP.start( 'your.smtp.server' ) {|smtp|
175 smtp.send_mail mail_string,
176 'from@mail.address',
177 'dest@mail.address' 'dest2@mail.address'
178 }
179
180 : ready( from_addr, *to_addrs ) {|adapter| .... }
181 This method stands by the SMTP object for sending mail and
182 gives adapter object to the block. ADAPTER has these 5 methods:
183
184 puts print printf write <<
185
186 FROM_ADDR must be a String, representing source mail address.
187 TO_ADDRS must be Strings or an Array of Strings, representing
188 destination mail addresses.
189
190 # example
191 Net::SMTP.start( 'your.smtp.server', 25 ) {|smtp|
192 smtp.ready( 'from@mail.addr', 'dest@mail.addr' ) {|f|
193 f.puts 'From: aamine@loveruby.net'
194 f.puts 'To: someone@somedomain.org'
195 f.puts 'Subject: test mail'
196 f.puts
197 f.puts 'This is test mail.'
198 }
199 }
200
201 == Exceptions
202
203 SMTP objects raise these exceptions:
204 : Net::ProtoSyntaxError
205 syntax error (errno.500)
206 : Net::ProtoFatalError
207 fatal error (errno.550)
208 : Net::ProtoUnknownError
209 unknown error. (is probably bug)
210 : Net::ProtoServerBusy
211 temporary error (errno.420/450)
212
213 =end
214
215 require 'net/protocol'
216 require 'digest/md5'
217
218
219 module Net
220
221 class SMTP < Protocol
222
223 protocol_param :default_port, '25'
224 protocol_param :command_type, '::Net::SMTPCommand'
225 protocol_param :socket_type, '::Net::InternetMessageIO'
226
227
228 def initialize( addr, port = nil )
229 super
230 @esmtp = true
231 end
232
233 def esmtp?
234 @esmtp
235 end
236
237 def esmtp=( bool )
238 @esmtp = bool
239 end
240
241 alias esmtp esmtp?
242
243 private
244
245 def do_start( helo = 'localhost.localdomain',
246 user = nil, secret = nil, authtype = nil )
247 conn_socket
248 conn_command
249
250 begin
251 if @esmtp then
252 command().ehlo helo
253 else
254 command().helo helo
255 end
256 rescue ProtocolError
257 if @esmtp then
258 @esmtp = false
259 command().error_ok
260 retry
261 else
262 raise
263 end
264 end
265
266 if user or secret then
267 (user and secret) or
268 raise ArgumentError, 'both of account and password are required'
269
270 mid = 'auth_' + (authtype || 'cram_md5').to_s
271 command().respond_to? mid or
272 raise ArgumentError, "wrong auth type #{authtype.to_s}"
273
274 command().__send__ mid, user, secret
275 end
276 end
277
278 def do_finish
279 disconn_command
280 disconn_socket
281 end
282
283
284 #
285 # SMTP operations
286 #
287
288 public
289
290 def send_mail( mailsrc, from_addr, *to_addrs )
291 do_ready from_addr, to_addrs.flatten
292 command().write_mail(mailsrc)
293 end
294
295 alias sendmail send_mail
296
297 def ready( from_addr, *to_addrs, &block )
298 do_ready from_addr, to_addrs.flatten
299 command().through_mail(&block)
300 end
301
302 private
303
304 def do_ready( from_addr, to_addrs )
305 raise ArgumentError, 'mail destination does not given' if to_addrs.empty?
306 command().mailfrom from_addr
307 command().rcpt(to_addrs)
308 end
309
310 end
311
312 SMTPSession = SMTP
313
314
315 class SMTPCommand < Command
316
317 def initialize( sock )
318 super
319 atomic {
320 check_reply SuccessCode
321 }
322 end
323
324 def helo( domain )
325 atomic {
326 getok sprintf('HELO %s', domain)
327 }
328 end
329
330 def ehlo( domain )
331 atomic {
332 getok sprintf('EHLO %s', domain)
333 }
334 end
335
336 # "PLAIN" authentication [RFC2554]
337 def auth_plain( user, secret )
338 atomic {
339 getok sprintf('AUTH PLAIN %s',
340 ["\0#{user}\0#{secret}"].pack('m').chomp)
341 }
342 end
343
344 # "CRAM-MD5" authentication [RFC2195]
345 def auth_cram_md5( user, secret )
346 atomic {
347 rep = getok( 'AUTH CRAM-MD5', ContinueCode )
348 challenge = rep.msg.split(' ')[1].unpack('m')[0]
349 secret = Digest::MD5.digest(secret) if secret.size > 64
350
351 isecret = secret + "\0" * (64 - secret.size)
352 osecret = isecret.dup
353 0.upto( 63 ) do |i|
354 isecret[i] ^= 0x36
355 osecret[i] ^= 0x5c
356 end
357 tmp = Digest::MD5.digest( isecret + challenge )
358 tmp = Digest::MD5.hexdigest( osecret + tmp )
359
360 getok [user + ' ' + tmp].pack('m').chomp
361 }
362 end
363
364 def mailfrom( fromaddr )
365 atomic {
366 getok sprintf('MAIL FROM:<%s>', fromaddr)
367 }
368 end
369
370 def rcpt( toaddrs )
371 toaddrs.each do |i|
372 atomic {
373 getok sprintf('RCPT TO:<%s>', i)
374 }
375 end
376 end
377
378 def write_mail( src )
379 atomic {
380 getok 'DATA', ContinueCode
381 @socket.write_message src
382 check_reply SuccessCode
383 }
384 end
385
386 def through_mail( &block )
387 atomic {
388 getok 'DATA', ContinueCode
389 @socket.through_message(&block)
390 check_reply SuccessCode
391 }
392 end
393
394 def quit
395 atomic {
396 getok 'QUIT'
397 }
398 end
399
400 private
401
402 def get_reply
403 arr = read_reply
404 stat = arr[0][0,3]
405
406 klass = case stat[0]
407 when ?2 then SuccessCode
408 when ?3 then ContinueCode
409 when ?4 then ServerErrorCode
410 when ?5 then
411 case stat[1]
412 when ?0 then SyntaxErrorCode
413 when ?3 then AuthErrorCode
414 when ?5 then FatalErrorCode
415 end
416 end
417 klass ||= UnknownCode
418
419 Response.new( klass, stat, arr.join('') )
420 end
421
422 def read_reply
423 arr = []
424 while true do
425 str = @socket.readline
426 break unless str[3] == ?- # ex: "210-..."
427 arr.push str
428 end
429 arr.push str
430
431 arr
432 end
433
434 end
435
436
437 # for backward compatibility
438 module NetPrivate
439 SMTPCommand = ::Net::SMTPCommand
440 end
441
442 end # module Net