1 # = net/smtp.rb
2 #
3 # Copyright (c) 1999-2007 Yukihiro Matsumoto.
4 #
5 # Copyright (c) 1999-2007 Minero Aoki.
6 #
7 # Written & maintained by Minero Aoki <aamine@loveruby.net>.
8 #
9 # Documented by William Webber and Minero Aoki.
10 #
11 # This program is free software. You can re-distribute and/or
12 # modify this program under the same terms as Ruby itself.
13 #
14 # NOTE: You can find Japanese version of this document at:
15 # http://www.ruby-lang.org/ja/man/html/net_smtp.html
16 #
17 # $Id: smtp.rb 28208 2010-06-08 06:14:59Z shyouhei $
18 #
19 # See Net::SMTP for documentation.
20 #
21
22 require 'net/protocol'
23 require 'digest/md5'
24 require 'timeout'
25 begin
26 require 'openssl'
27 rescue LoadError
28 end
29
30 module Net
31
32 # Module mixed in to all SMTP error classes
33 module SMTPError
34 # This *class* is a module for backward compatibility.
35 # In later release, this module becomes a class.
36 end
37
38 # Represents an SMTP authentication error.
39 class SMTPAuthenticationError < ProtoAuthError
40 include SMTPError
41 end
42
43 # Represents SMTP error code 420 or 450, a temporary error.
44 class SMTPServerBusy < ProtoServerError
45 include SMTPError
46 end
47
48 # Represents an SMTP command syntax error (error code 500)
49 class SMTPSyntaxError < ProtoSyntaxError
50 include SMTPError
51 end
52
53 # Represents a fatal SMTP error (error code 5xx, except for 500)
54 class SMTPFatalError < ProtoFatalError
55 include SMTPError
56 end
57
58 # Unexpected reply code returned from server.
59 class SMTPUnknownError < ProtoUnknownError
60 include SMTPError
61 end
62
63 # Command is not supported on server.
64 class SMTPUnsupportedCommand < ProtocolError
65 include SMTPError
66 end
67
68 #
69 # = Net::SMTP
70 #
71 # == What is This Library?
72 #
73 # This library provides functionality to send internet
74 # mail via SMTP, the Simple Mail Transfer Protocol. For details of
75 # SMTP itself, see [RFC2821] (http://www.ietf.org/rfc/rfc2821.txt).
76 #
77 # == What is This Library NOT?
78 #
79 # This library does NOT provide functions to compose internet mails.
80 # You must create them by yourself. If you want better mail support,
81 # try RubyMail or TMail. You can get both libraries from RAA.
82 # (http://www.ruby-lang.org/en/raa.html)
83 #
84 # FYI: the official documentation on internet mail is: [RFC2822] (http://www.ietf.org/rfc/rfc2822.txt).
85 #
86 # == Examples
87 #
88 # === Sending Messages
89 #
90 # You must open a connection to an SMTP server before sending messages.
91 # The first argument is the address of your SMTP server, and the second
92 # argument is the port number. Using SMTP.start with a block is the simplest
93 # way to do this. This way, the SMTP connection is closed automatically
94 # after the block is executed.
95 #
96 # require 'net/smtp'
97 # Net::SMTP.start('your.smtp.server', 25) do |smtp|
98 # # Use the SMTP object smtp only in this block.
99 # end
100 #
101 # Replace 'your.smtp.server' with your SMTP server. Normally
102 # your system manager or internet provider supplies a server
103 # for you.
104 #
105 # Then you can send messages.
106 #
107 # msgstr = <<END_OF_MESSAGE
108 # From: Your Name <your@mail.address>
109 # To: Destination Address <someone@example.com>
110 # Subject: test message
111 # Date: Sat, 23 Jun 2001 16:26:43 +0900
112 # Message-Id: <unique.message.id.string@example.com>
113 #
114 # This is a test message.
115 # END_OF_MESSAGE
116 #
117 # require 'net/smtp'
118 # Net::SMTP.start('your.smtp.server', 25) do |smtp|
119 # smtp.send_message msgstr,
120 # 'your@mail.address',
121 # 'his_addess@example.com'
122 # end
123 #
124 # === Closing the Session
125 #
126 # You MUST close the SMTP session after sending messages, by calling
127 # the #finish method:
128 #
129 # # using SMTP#finish
130 # smtp = Net::SMTP.start('your.smtp.server', 25)
131 # smtp.send_message msgstr, 'from@address', 'to@address'
132 # smtp.finish
133 #
134 # You can also use the block form of SMTP.start/SMTP#start. This closes
135 # the SMTP session automatically:
136 #
137 # # using block form of SMTP.start
138 # Net::SMTP.start('your.smtp.server', 25) do |smtp|
139 # smtp.send_message msgstr, 'from@address', 'to@address'
140 # end
141 #
142 # I strongly recommend this scheme. This form is simpler and more robust.
143 #
144 # === HELO domain
145 #
146 # In almost all situations, you must provide a third argument
147 # to SMTP.start/SMTP#start. This is the domain name which you are on
148 # (the host to send mail from). It is called the "HELO domain".
149 # The SMTP server will judge whether it should send or reject
150 # the SMTP session by inspecting the HELO domain.
151 #
152 # Net::SMTP.start('your.smtp.server', 25,
153 # 'mail.from.domain') { |smtp| ... }
154 #
155 # === SMTP Authentication
156 #
157 # The Net::SMTP class supports three authentication schemes;
158 # PLAIN, LOGIN and CRAM MD5. (SMTP Authentication: [RFC2554])
159 # To use SMTP authentication, pass extra arguments to
160 # SMTP.start/SMTP#start.
161 #
162 # # PLAIN
163 # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain',
164 # 'Your Account', 'Your Password', :plain)
165 # # LOGIN
166 # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain',
167 # 'Your Account', 'Your Password', :login)
168 #
169 # # CRAM MD5
170 # Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain',
171 # 'Your Account', 'Your Password', :cram_md5)
172 #
173 class SMTP
174
175 Revision = %q$Revision: 28208 $.split[1]
176
177 # The default SMTP port number, 25.
178 def SMTP.default_port
179 25
180 end
181
182 # The default mail submission port number, 587.
183 def SMTP.default_submission_port
184 587
185 end
186
187 # The default SMTPS port number, 465.
188 def SMTP.default_tls_port
189 465
190 end
191
192 class << self
193 alias default_ssl_port default_tls_port
194 end
195
196 def SMTP.default_ssl_context
197 OpenSSL::SSL::SSLContext.new
198 end
199
200 #
201 # Creates a new Net::SMTP object.
202 #
203 # +address+ is the hostname or ip address of your SMTP
204 # server. +port+ is the port to connect to; it defaults to
205 # port 25.
206 #
207 # This method does not open the TCP connection. You can use
208 # SMTP.start instead of SMTP.new if you want to do everything
209 # at once. Otherwise, follow SMTP.new with SMTP#start.
210 #
211 def initialize(address, port = nil)
212 @address = address
213 @port = (port || SMTP.default_port)
214 @esmtp = true
215 @capabilities = nil
216 @socket = nil
217 @started = false
218 @open_timeout = 30
219 @read_timeout = 60
220 @error_occured = false
221 @debug_output = nil
222 @tls = false
223 @starttls = false
224 @ssl_context = nil
225 end
226
227 # Provide human-readable stringification of class state.
228 def inspect
229 "#<#{self.class} #{@address}:#{@port} started=#{@started}>"
230 end
231
232 # +true+ if the SMTP object uses ESMTP (which it does by default).
233 def esmtp?
234 @esmtp
235 end
236
237 #
238 # Set whether to use ESMTP or not. This should be done before
239 # calling #start. Note that if #start is called in ESMTP mode,
240 # and the connection fails due to a ProtocolError, the SMTP
241 # object will automatically switch to plain SMTP mode and
242 # retry (but not vice versa).
243 #
244 def esmtp=(bool)
245 @esmtp = bool
246 end
247
248 alias esmtp esmtp?
249
250 # true if server advertises STARTTLS.
251 # You cannot get valid value before opening SMTP session.
252 def capable_starttls?
253 capable?('STARTTLS')
254 end
255
256 def capable?(key)
257 return nil unless @capabilities
258 @capabilities[key] ? true : false
259 end
260 private :capable?
261
262 # true if server advertises AUTH PLAIN.
263 # You cannot get valid value before opening SMTP session.
264 def capable_plain_auth?
265 auth_capable?('PLAIN')
266 end
267
268 # true if server advertises AUTH LOGIN.
269 # You cannot get valid value before opening SMTP session.
270 def capable_login_auth?
271 auth_capable?('LOGIN')
272 end
273
274 # true if server advertises AUTH CRAM-MD5.
275 # You cannot get valid value before opening SMTP session.
276 def capable_cram_md5_auth?
277 auth_capable?('CRAM-MD5')
278 end
279
280 def auth_capable?(type)
281 return nil unless @capabilities
282 return false unless @capabilities['AUTH']
283 @capabilities['AUTH'].include?(type)
284 end
285 private :auth_capable?
286
287 # Returns supported authentication methods on this server.
288 # You cannot get valid value before opening SMTP session.
289 def capable_auth_types
290 return [] unless @capabilities
291 return [] unless @capabilities['AUTH']
292 @capabilities['AUTH']
293 end
294
295 # true if this object uses SMTP/TLS (SMTPS).
296 def tls?
297 @tls
298 end
299
300 alias ssl? tls?
301
302 # Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for
303 # this object. Must be called before the connection is established
304 # to have any effect. +context+ is a OpenSSL::SSL::SSLContext object.
305 def enable_tls(context = SMTP.default_ssl_context)
306 raise 'openssl library not installed' unless defined?(OpenSSL)
307 raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls
308 @tls = true
309 @ssl_context = context
310 end
311
312 alias enable_ssl enable_tls
313
314 # Disables SMTP/TLS for this object. Must be called before the
315 # connection is established to have any effect.
316 def disable_tls
317 @tls = false
318 @ssl_context = nil
319 end
320
321 alias disable_ssl disable_tls
322
323 # Returns truth value if this object uses STARTTLS.
324 # If this object always uses STARTTLS, returns :always.
325 # If this object uses STARTTLS when the server support TLS, returns :auto.
326 def starttls?
327 @starttls
328 end
329
330 # true if this object uses STARTTLS.
331 def starttls_always?
332 @starttls == :always
333 end
334
335 # true if this object uses STARTTLS when server advertises STARTTLS.
336 def starttls_auto?
337 @starttls == :auto
338 end
339
340 # Enables SMTP/TLS (STARTTLS) for this object.
341 # +context+ is a OpenSSL::SSL::SSLContext object.
342 def enable_starttls(context = SMTP.default_ssl_context)
343 raise 'openssl library not installed' unless defined?(OpenSSL)
344 raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
345 @starttls = :always
346 @ssl_context = context
347 end
348
349 # Enables SMTP/TLS (STARTTLS) for this object if server accepts.
350 # +context+ is a OpenSSL::SSL::SSLContext object.
351 def enable_starttls_auto(context = SMTP.default_ssl_context)
352 raise 'openssl library not installed' unless defined?(OpenSSL)
353 raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
354 @starttls = :auto
355 @ssl_context = context
356 end
357
358 # Disables SMTP/TLS (STARTTLS) for this object. Must be called
359 # before the connection is established to have any effect.
360 def disable_starttls
361 @starttls = false
362 @ssl_context = nil
363 end
364
365 # The address of the SMTP server to connect to.
366 attr_reader :address
367
368 # The port number of the SMTP server to connect to.
369 attr_reader :port
370
371 # Seconds to wait while attempting to open a connection.
372 # If the connection cannot be opened within this time, a
373 # TimeoutError is raised.
374 attr_accessor :open_timeout
375
376 # Seconds to wait while reading one block (by one read(2) call).
377 # If the read(2) call does not complete within this time, a
378 # TimeoutError is raised.
379 attr_reader :read_timeout
380
381 # Set the number of seconds to wait until timing-out a read(2)
382 # call.
383 def read_timeout=(sec)
384 @socket.read_timeout = sec if @socket
385 @read_timeout = sec
386 end
387
388 #
389 # WARNING: This method causes serious security holes.
390 # Use this method for only debugging.
391 #
392 # Set an output stream for debug logging.
393 # You must call this before #start.
394 #
395 # # example
396 # smtp = Net::SMTP.new(addr, port)
397 # smtp.set_debug_output $stderr
398 # smtp.start do |smtp|
399 # ....
400 # end
401 #
402 def debug_output=(arg)
403 @debug_output = arg
404 end
405
406 alias set_debug_output debug_output=
407
408 #
409 # SMTP session control
410 #
411
412 #
413 # Creates a new Net::SMTP object and connects to the server.
414 #
415 # This method is equivalent to:
416 #
417 # Net::SMTP.new(address, port).start(helo_domain, account, password, authtype)
418 #
419 # === Example
420 #
421 # Net::SMTP.start('your.smtp.server') do |smtp|
422 # smtp.send_message msgstr, 'from@example.com', ['dest@example.com']
423 # end
424 #
425 # === Block Usage
426 #
427 # If called with a block, the newly-opened Net::SMTP object is yielded
428 # to the block, and automatically closed when the block finishes. If called
429 # without a block, the newly-opened Net::SMTP object is returned to
430 # the caller, and it is the caller's responsibility to close it when
431 # finished.
432 #
433 # === Parameters
434 #
435 # +address+ is the hostname or ip address of your smtp server.
436 #
437 # +port+ is the port to connect to; it defaults to port 25.
438 #
439 # +helo+ is the _HELO_ _domain_ provided by the client to the
440 # server (see overview comments); it defaults to 'localhost.localdomain'.
441 #
442 # The remaining arguments are used for SMTP authentication, if required
443 # or desired. +user+ is the account name; +secret+ is your password
444 # or other authentication token; and +authtype+ is the authentication
445 # type, one of :plain, :login, or :cram_md5. See the discussion of
446 # SMTP Authentication in the overview notes.
447 #
448 # === Errors
449 #
450 # This method may raise:
451 #
452 # * Net::SMTPAuthenticationError
453 # * Net::SMTPServerBusy
454 # * Net::SMTPSyntaxError
455 # * Net::SMTPFatalError
456 # * Net::SMTPUnknownError
457 # * IOError
458 # * TimeoutError
459 #
460 def SMTP.start(address, port = nil, helo = 'localhost.localdomain',
461 user = nil, secret = nil, authtype = nil,
462 &block) # :yield: smtp
463 new(address, port).start(helo, user, secret, authtype, &block)
464 end
465
466 # +true+ if the SMTP session has been started.
467 def started?
468 @started
469 end
470
471 #
472 # Opens a TCP connection and starts the SMTP session.
473 #
474 # === Parameters
475 #
476 # +helo+ is the _HELO_ _domain_ that you'll dispatch mails from; see
477 # the discussion in the overview notes.
478 #
479 # If both of +user+ and +secret+ are given, SMTP authentication
480 # will be attempted using the AUTH command. +authtype+ specifies
481 # the type of authentication to attempt; it must be one of
482 # :login, :plain, and :cram_md5. See the notes on SMTP Authentication
483 # in the overview.
484 #
485 # === Block Usage
486 #
487 # When this methods is called with a block, the newly-started SMTP
488 # object is yielded to the block, and automatically closed after
489 # the block call finishes. Otherwise, it is the caller's
490 # responsibility to close the session when finished.
491 #
492 # === Example
493 #
494 # This is very similar to the class method SMTP.start.
495 #
496 # require 'net/smtp'
497 # smtp = Net::SMTP.new('smtp.mail.server', 25)
498 # smtp.start(helo_domain, account, password, authtype) do |smtp|
499 # smtp.send_message msgstr, 'from@example.com', ['dest@example.com']
500 # end
501 #
502 # The primary use of this method (as opposed to SMTP.start)
503 # is probably to set debugging (#set_debug_output) or ESMTP
504 # (#esmtp=), which must be done before the session is
505 # started.
506 #
507 # === Errors
508 #
509 # If session has already been started, an IOError will be raised.
510 #
511 # This method may raise:
512 #
513 # * Net::SMTPAuthenticationError
514 # * Net::SMTPServerBusy
515 # * Net::SMTPSyntaxError
516 # * Net::SMTPFatalError
517 # * Net::SMTPUnknownError
518 # * IOError
519 # * TimeoutError
520 #
521 def start(helo = 'localhost.localdomain',
522 user = nil, secret = nil, authtype = nil) # :yield: smtp
523 if block_given?
524 begin
525 do_start helo, user, secret, authtype
526 return yield(self)
527 ensure
528 do_finish
529 end
530 else
531 do_start helo, user, secret, authtype
532 return self
533 end
534 end
535
536 # Finishes the SMTP session and closes TCP connection.
537 # Raises IOError if not started.
538 def finish
539 raise IOError, 'not yet started' unless started?
540 do_finish
541 end
542
543 private
544
545 def do_start(helo_domain, user, secret, authtype)
546 raise IOError, 'SMTP session already started' if @started
547 if user or secret
548 check_auth_method(authtype || DEFAULT_AUTH_TYPE)
549 check_auth_args user, secret
550 end
551 s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
552 logging "Connection opened: #{@address}:#{@port}"
553 @socket = new_internet_message_io(tls? ? tlsconnect(s) : s)
554 check_response critical { recv_response() }
555 do_helo helo_domain
556 if starttls_always? or (capable_starttls? and starttls_auto?)
557 unless capable_starttls?
558 raise SMTPUnsupportedCommand,
559 "STARTTLS is not supported on this server"
560 end
561 starttls
562 @socket = new_internet_message_io(tlsconnect(s))
563 # helo response may be different after STARTTLS
564 do_helo helo_domain
565 end
566 authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user
567 @started = true
568 ensure
569 unless @started
570 # authentication failed, cancel connection.
571 s.close if s and not s.closed?
572 @socket = nil
573 end
574 end
575
576 def tlsconnect(s)
577 s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
578 logging "TLS connection started"
579 s.sync_close = true
580 s.connect
581 if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
582 s.post_connection_check(@address)
583 end
584 s
585 end
586
587 def new_internet_message_io(s)
588 io = InternetMessageIO.new(s)
589 io.read_timeout = @read_timeout
590 io.debug_output = @debug_output
591 io
592 end
593
594 def do_helo(helo_domain)
595 res = @esmtp ? ehlo(helo_domain) : helo(helo_domain)
596 @capabilities = res.capabilities
597 rescue SMTPError
598 if @esmtp
599 @esmtp = false
600 @error_occured = false
601 retry
602 end
603 raise
604 end
605
606 def do_finish
607 quit if @socket and not @socket.closed? and not @error_occured
608 ensure
609 @started = false
610 @error_occured = false
611 @socket.close if @socket and not @socket.closed?
612 @socket = nil
613 end
614
615 #
616 # Message Sending
617 #
618
619 public
620
621 #
622 # Sends +msgstr+ as a message. Single CR ("\r") and LF ("\n") found
623 # in the +msgstr+, are converted into the CR LF pair. You cannot send a
624 # binary message with this method. +msgstr+ should include both
625 # the message headers and body.
626 #
627 # +from_addr+ is a String representing the source mail address.
628 #
629 # +to_addr+ is a String or Strings or Array of Strings, representing
630 # the destination mail address or addresses.
631 #
632 # === Example
633 #
634 # Net::SMTP.start('smtp.example.com') do |smtp|
635 # smtp.send_message msgstr,
636 # 'from@example.com',
637 # ['dest@example.com', 'dest2@example.com']
638 # end
639 #
640 # === Errors
641 #
642 # This method may raise:
643 #
644 # * Net::SMTPServerBusy
645 # * Net::SMTPSyntaxError
646 # * Net::SMTPFatalError
647 # * Net::SMTPUnknownError
648 # * IOError
649 # * TimeoutError
650 #
651 def send_message(msgstr, from_addr, *to_addrs)
652 raise IOError, 'closed session' unless @socket
653 mailfrom from_addr
654 rcptto_list(to_addrs) {data msgstr}
655 end
656
657 alias send_mail send_message
658 alias sendmail send_message # obsolete
659
660 #
661 # Opens a message writer stream and gives it to the block.
662 # The stream is valid only in the block, and has these methods:
663 #
664 # puts(str = ''):: outputs STR and CR LF.
665 # print(str):: outputs STR.
666 # printf(fmt, *args):: outputs sprintf(fmt,*args).
667 # write(str):: outputs STR and returns the length of written bytes.
668 # <<(str):: outputs STR and returns self.
669 #
670 # If a single CR ("\r") or LF ("\n") is found in the message,
671 # it is converted to the CR LF pair. You cannot send a binary
672 # message with this method.
673 #
674 # === Parameters
675 #
676 # +from_addr+ is a String representing the source mail address.
677 #
678 # +to_addr+ is a String or Strings or Array of Strings, representing
679 # the destination mail address or addresses.
680 #
681 # === Example
682 #
683 # Net::SMTP.start('smtp.example.com', 25) do |smtp|
684 # smtp.open_message_stream('from@example.com', ['dest@example.com']) do |f|
685 # f.puts 'From: from@example.com'
686 # f.puts 'To: dest@example.com'
687 # f.puts 'Subject: test message'
688 # f.puts
689 # f.puts 'This is a test message.'
690 # end
691 # end
692 #
693 # === Errors
694 #
695 # This method may raise:
696 #
697 # * Net::SMTPServerBusy
698 # * Net::SMTPSyntaxError
699 # * Net::SMTPFatalError
700 # * Net::SMTPUnknownError
701 # * IOError
702 # * TimeoutError
703 #
704 def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream
705 raise IOError, 'closed session' unless @socket
706 mailfrom from_addr
707 rcptto_list(to_addrs) {data(&block)}
708 end
709
710 alias ready open_message_stream # obsolete
711
712 #
713 # Authentication
714 #
715
716 public
717
718 DEFAULT_AUTH_TYPE = :plain
719
720 def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE)
721 check_auth_method authtype
722 check_auth_args user, secret
723 send auth_method(authtype), user, secret
724 end
725
726 def auth_plain(user, secret)
727 check_auth_args user, secret
728 res = critical {
729 get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}"))
730 }
731 check_auth_response res
732 res
733 end
734
735 def auth_login(user, secret)
736 check_auth_args user, secret
737 res = critical {
738 check_auth_continue get_response('AUTH LOGIN')
739 check_auth_continue get_response(base64_encode(user))
740 get_response(base64_encode(secret))
741 }
742 check_auth_response res
743 res
744 end
745
746 def auth_cram_md5(user, secret)
747 check_auth_args user, secret
748 res = critical {
749 res0 = get_response('AUTH CRAM-MD5')
750 check_auth_continue res0
751 crammed = cram_md5_response(secret, res0.cram_md5_challenge)
752 get_response(base64_encode("#{user} #{crammed}"))
753 }
754 check_auth_response res
755 res
756 end
757
758 private
759
760 def check_auth_method(type)
761 unless respond_to?(auth_method(type), true)
762 raise ArgumentError, "wrong authentication type #{type}"
763 end
764 end
765
766 def auth_method(type)
767 "auth_#{type.to_s.downcase}".intern
768 end
769
770 def check_auth_args(user, secret)
771 unless user
772 raise ArgumentError, 'SMTP-AUTH requested but missing user name'
773 end
774 unless secret
775 raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase'
776 end
777 end
778
779 def base64_encode(str)
780 # expects "str" may not become too long
781 [str].pack('m').gsub(/\s+/, '')
782 end
783
784 IMASK = 0x36
785 OMASK = 0x5c
786
787 # CRAM-MD5: [RFC2195]
788 def cram_md5_response(secret, challenge)
789 tmp = Digest::MD5.digest(cram_secret(secret, IMASK) + challenge)
790 Digest::MD5.hexdigest(cram_secret(secret, OMASK) + tmp)
791 end
792
793 CRAM_BUFSIZE = 64
794
795 def cram_secret(secret, mask)
796 secret = Digest::MD5.digest(secret) if secret.size > CRAM_BUFSIZE
797 buf = secret.ljust(CRAM_BUFSIZE, "\0")
798 0.upto(buf.size - 1) do |i|
799 buf[i] = (buf[i].ord ^ mask).chr
800 end
801 buf
802 end
803
804 #
805 # SMTP command dispatcher
806 #
807
808 public
809
810 def starttls
811 getok('STARTTLS')
812 end
813
814 def helo(domain)
815 getok("HELO #{domain}")
816 end
817
818 def ehlo(domain)
819 getok("EHLO #{domain}")
820 end
821
822 def mailfrom(from_addr)
823 if $SAFE > 0
824 raise SecurityError, 'tainted from_addr' if from_addr.tainted?
825 end
826 getok("MAIL FROM:<#{from_addr}>")
827 end
828
829 def rcptto_list(to_addrs)
830 raise ArgumentError, 'mail destination not given' if to_addrs.empty?
831 ok_users = []
832 unknown_users = []
833 to_addrs.flatten.each do |addr|
834 begin
835 rcptto addr
836 rescue SMTPAuthenticationError
837 unknown_users << addr.dump
838 else
839 ok_users << addr
840 end
841 end
842 raise ArgumentError, 'mail destination not given' if ok_users.empty?
843 ret = yield
844 unless unknown_users.empty?
845 raise SMTPAuthenticationError, "failed to deliver for #{unknown_users.join(', ')}"
846 end
847 ret
848 end
849
850 def rcptto(to_addr)
851 if $SAFE > 0
852 raise SecurityError, 'tainted to_addr' if to_addr.tainted?
853 end
854 getok("RCPT TO:<#{to_addr}>")
855 end
856
857 # This method sends a message.
858 # If +msgstr+ is given, sends it as a message.
859 # If block is given, yield a message writer stream.
860 # You must write message before the block is closed.
861 #
862 # # Example 1 (by string)
863 # smtp.data(<<EndMessage)
864 # From: john@example.com
865 # To: betty@example.com
866 # Subject: I found a bug
867 #
868 # Check vm.c:58879.
869 # EndMessage
870 #
871 # # Example 2 (by block)
872 # smtp.data {|f|
873 # f.puts "From: john@example.com"
874 # f.puts "To: betty@example.com"
875 # f.puts "Subject: I found a bug"
876 # f.puts ""
877 # f.puts "Check vm.c:58879."
878 # }
879 #
880 def data(msgstr = nil, &block) #:yield: stream
881 if msgstr and block
882 raise ArgumentError, "message and block are exclusive"
883 end
884 unless msgstr or block
885 raise ArgumentError, "message or block is required"
886 end
887 res = critical {
888 check_continue get_response('DATA')
889 if msgstr
890 @socket.write_message msgstr
891 else
892 @socket.write_message_by_block(&block)
893 end
894 recv_response()
895 }
896 check_response res
897 res
898 end
899
900 def quit
901 getok('QUIT')
902 end
903
904 private
905
906 def getok(reqline)
907 res = critical {
908 @socket.writeline reqline
909 recv_response()
910 }
911 check_response res
912 res
913 end
914
915 def get_response(reqline)
916 @socket.writeline reqline
917 recv_response()
918 end
919
920 def recv_response
921 buf = ''
922 while true
923 line = @socket.readline
924 buf << line << "\n"
925 break unless line[3,1] == '-' # "210-PIPELINING"
926 end
927 Response.parse(buf)
928 end
929
930 def critical(&block)
931 return '200 dummy reply code' if @error_occured
932 begin
933 return yield()
934 rescue Exception
935 @error_occured = true
936 raise
937 end
938 end
939
940 def check_response(res)
941 unless res.success?
942 raise res.exception_class, res.message
943 end
944 end
945
946 def check_continue(res)
947 unless res.continue?
948 raise SMTPUnknownError, "could not get 3xx (#{res.status})"
949 end
950 end
951
952 def check_auth_response(res)
953 unless res.success?
954 raise SMTPAuthenticationError, res.message
955 end
956 end
957
958 def check_auth_continue(res)
959 unless res.continue?
960 raise res.exception_class, res.message
961 end
962 end
963
964 class Response
965 def Response.parse(str)
966 new(str[0,3], str)
967 end
968
969 def initialize(status, string)
970 @status = status
971 @string = string
972 end
973
974 attr_reader :status
975 attr_reader :string
976
977 def status_type_char
978 @status[0, 1]
979 end
980
981 def success?
982 status_type_char() == '2'
983 end
984
985 def continue?
986 status_type_char() == '3'
987 end
988
989 def message
990 @string.lines.first
991 end
992
993 def cram_md5_challenge
994 @string.split(/ /)[1].unpack('m')[0]
995 end
996
997 def capabilities
998 return {} unless @string[3, 1] == '-'
999 h = {}
1000 @string.lines.drop(1).each do |line|
1001 k, *v = line[4..-1].chomp.split(nil)
1002 h[k] = v
1003 end
1004 h
1005 end
1006
1007 def exception_class
1008 case @status
1009 when /\A4/ then SMTPServerBusy
1010 when /\A50/ then SMTPSyntaxError
1011 when /\A53/ then SMTPAuthenticationError
1012 when /\A5/ then SMTPFatalError
1013 else SMTPUnknownError
1014 end
1015 end
1016 end
1017
1018 def logging(msg)
1019 @debug_output << msg + "\n" if @debug_output
1020 end
1021
1022 end # class SMTP
1023
1024 SMTPSession = SMTP
1025
1026 end