Project

General

Profile

RE: Problems setting up email notifications » smtp.rb

Sam Chen, 2011-12-12 11:41

 
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
require 'net/ntlm'
26
begin
27
  require 'openssl'
28
rescue LoadError
29
end
30

    
31
module Net
32

    
33
  # Module mixed in to all SMTP error classes
34
  module SMTPError
35
    # This *class* is a module for backward compatibility.
36
    # In later release, this module becomes a class.
37
  end
38

    
39
  # Represents an SMTP authentication error.
40
  class SMTPAuthenticationError < ProtoAuthError
41
    include SMTPError
42
  end
43

    
44
  # Represents SMTP error code 420 or 450, a temporary error.
45
  class SMTPServerBusy < ProtoServerError
46
    include SMTPError
47
  end
48

    
49
  # Represents an SMTP command syntax error (error code 500)
50
  class SMTPSyntaxError < ProtoSyntaxError
51
    include SMTPError
52
  end
53

    
54
  # Represents a fatal SMTP error (error code 5xx, except for 500)
55
  class SMTPFatalError < ProtoFatalError
56
    include SMTPError
57
  end
58

    
59
  # Unexpected reply code returned from server.
60
  class SMTPUnknownError < ProtoUnknownError
61
    include SMTPError
62
  end
63

    
64
  # Command is not supported on server.
65
  class SMTPUnsupportedCommand < ProtocolError
66
    include SMTPError
67
  end
68

    
69
  #
70
  # = Net::SMTP
71
  #
72
  # == What is This Library?
73
  # 
74
  # This library provides functionality to send internet
75
  # mail via SMTP, the Simple Mail Transfer Protocol. For details of
76
  # SMTP itself, see [RFC2821] (http://www.ietf.org/rfc/rfc2821.txt).
77
  # 
78
  # == What is This Library NOT?
79
  # 
80
  # This library does NOT provide functions to compose internet mails.
81
  # You must create them by yourself. If you want better mail support,
82
  # try RubyMail or TMail. You can get both libraries from RAA.
83
  # (http://www.ruby-lang.org/en/raa.html)
84
  # 
85
  # FYI: the official documentation on internet mail is: [RFC2822] (http://www.ietf.org/rfc/rfc2822.txt).
86
  # 
87
  # == Examples
88
  # 
89
  # === Sending Messages
90
  # 
91
  # You must open a connection to an SMTP server before sending messages.
92
  # The first argument is the address of your SMTP server, and the second 
93
  # argument is the port number. Using SMTP.start with a block is the simplest 
94
  # way to do this. This way, the SMTP connection is closed automatically 
95
  # after the block is executed.
96
  # 
97
  #     require 'net/smtp'
98
  #     Net::SMTP.start('your.smtp.server', 25) do |smtp|
99
  #       # Use the SMTP object smtp only in this block.
100
  #     end
101
  # 
102
  # Replace 'your.smtp.server' with your SMTP server. Normally
103
  # your system manager or internet provider supplies a server
104
  # for you.
105
  # 
106
  # Then you can send messages.
107
  # 
108
  #     msgstr = <<END_OF_MESSAGE
109
  #     From: Your Name <your@mail.address>
110
  #     To: Destination Address <someone@example.com>
111
  #     Subject: test message
112
  #     Date: Sat, 23 Jun 2001 16:26:43 +0900
113
  #     Message-Id: <unique.message.id.string@example.com>
114
  # 
115
  #     This is a test message.
116
  #     END_OF_MESSAGE
117
  # 
118
  #     require 'net/smtp'
119
  #     Net::SMTP.start('your.smtp.server', 25) do |smtp|
120
  #       smtp.send_message msgstr,
121
  #                         'your@mail.address',
122
  #                         'his_addess@example.com'
123
  #     end
124
  # 
125
  # === Closing the Session
126
  # 
127
  # You MUST close the SMTP session after sending messages, by calling 
128
  # the #finish method:
129
  # 
130
  #     # using SMTP#finish
131
  #     smtp = Net::SMTP.start('your.smtp.server', 25)
132
  #     smtp.send_message msgstr, 'from@address', 'to@address'
133
  #     smtp.finish
134
  # 
135
  # You can also use the block form of SMTP.start/SMTP#start.  This closes
136
  # the SMTP session automatically:
137
  # 
138
  #     # using block form of SMTP.start
139
  #     Net::SMTP.start('your.smtp.server', 25) do |smtp|
140
  #       smtp.send_message msgstr, 'from@address', 'to@address'
141
  #     end
142
  # 
143
  # I strongly recommend this scheme.  This form is simpler and more robust.
144
  # 
145
  # === HELO domain
146
  # 
147
  # In almost all situations, you must provide a third argument
148
  # to SMTP.start/SMTP#start. This is the domain name which you are on
149
  # (the host to send mail from). It is called the "HELO domain".
150
  # The SMTP server will judge whether it should send or reject
151
  # the SMTP session by inspecting the HELO domain.
152
  # 
153
  #     Net::SMTP.start('your.smtp.server', 25,
154
  #                     'mail.from.domain') { |smtp| ... }
155
  # 
156
  # === SMTP Authentication
157
  # 
158
  # The Net::SMTP class supports three authentication schemes;
159
  # PLAIN, LOGIN, and CRAM MD5.  (SMTP Authentication: [RFC2554])
160
  # To use SMTP authentication, pass extra arguments to 
161
  # SMTP.start/SMTP#start.
162
  # 
163
  #     # PLAIN
164
  #     Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain',
165
  #                     'Your Account', 'Your Password', :plain)
166
  #     # LOGIN
167
  #     Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain',
168
  #                     'Your Account', 'Your Password', :login)
169
  # 
170
  #     # CRAM MD5
171
  #     Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain',
172
  #                     'Your Account', 'Your Password', :cram_md5)
173
  #
174
  class SMTP
175

    
176
    Revision = %q$Revision: 28208 $.split[1]
177

    
178
    # The default SMTP port number, 25.
179
    def SMTP.default_port
180
      25
181
    end
182

    
183
    # The default mail submission port number, 587.
184
    def SMTP.default_submission_port
185
      587
186
    end
187

    
188
    # The default SMTPS port number, 465.
189
    def SMTP.default_tls_port
190
      465
191
    end
192

    
193
    class << self
194
      alias default_ssl_port default_tls_port
195
    end
196

    
197
    def SMTP.default_ssl_context
198
      OpenSSL::SSL::SSLContext.new
199
    end
200
    
201
    #
202
    # Creates a new Net::SMTP object.
203
    #
204
    # +address+ is the hostname or ip address of your SMTP
205
    # server.  +port+ is the port to connect to; it defaults to
206
    # port 25.
207
    #
208
    # This method does not open the TCP connection.  You can use
209
    # SMTP.start instead of SMTP.new if you want to do everything
210
    # at once.  Otherwise, follow SMTP.new with SMTP#start.
211
    #
212
    def initialize(address, port = nil)
213
      @address = address
214
      @port = (port || SMTP.default_port)
215
      @esmtp = true
216
      @capabilities = nil
217
      @socket = nil
218
      @started = false
219
      @open_timeout = 30
220
      @read_timeout = 60
221
      @error_occured = false
222
      @debug_output = nil
223
      @tls = false
224
      @starttls = false
225
      @ssl_context = nil
226
    end
227
    
228
    # Provide human-readable stringification of class state.
229
    def inspect
230
      "#<#{self.class} #{@address}:#{@port} started=#{@started}>"
231
    end
232

    
233
    # +true+ if the SMTP object uses ESMTP (which it does by default).
234
    def esmtp?
235
      @esmtp
236
    end
237

    
238
    #
239
    # Set whether to use ESMTP or not.  This should be done before 
240
    # calling #start.  Note that if #start is called in ESMTP mode,
241
    # and the connection fails due to a ProtocolError, the SMTP
242
    # object will automatically switch to plain SMTP mode and
243
    # retry (but not vice versa).
244
    #
245
    def esmtp=(bool)
246
      @esmtp = bool
247
    end
248

    
249
    alias esmtp esmtp?
250

    
251
    # true if server advertises STARTTLS.
252
    # You cannot get valid value before opening SMTP session.
253
    def capable_starttls?
254
      capable?('STARTTLS')
255
    end
256

    
257
    def capable?(key)
258
      return nil unless @capabilities
259
      @capabilities[key] ? true : false
260
    end
261
    private :capable?
262

    
263
    # true if server advertises AUTH PLAIN.
264
    # You cannot get valid value before opening SMTP session.
265
    def capable_plain_auth?
266
      auth_capable?('PLAIN')
267
    end
268

    
269
    # true if server advertises AUTH LOGIN.
270
    # You cannot get valid value before opening SMTP session.
271
    def capable_login_auth?
272
      auth_capable?('LOGIN')
273
    end
274

    
275
    # true if server advertises AUTH NTLM.
276
    # You cannot get valid value before opening SMTP session.
277
    def capable_login_auth?
278
      auth_capable?('NTLM')
279
    end
280

    
281
    # true if server advertises AUTH CRAM-MD5.
282
    # You cannot get valid value before opening SMTP session.
283
    def capable_cram_md5_auth?
284
      auth_capable?('CRAM-MD5')
285
    end
286

    
287
    def auth_capable?(type)
288
      return nil unless @capabilities
289
      return false unless @capabilities['AUTH']
290
      @capabilities['AUTH'].include?(type)
291
    end
292
    private :auth_capable?
293

    
294
    # Returns supported authentication methods on this server.
295
    # You cannot get valid value before opening SMTP session.
296
    def capable_auth_types
297
      return [] unless @capabilities
298
      return [] unless @capabilities['AUTH']
299
      @capabilities['AUTH']
300
    end
301

    
302
    # true if this object uses SMTP/TLS (SMTPS).
303
    def tls?
304
      @tls
305
    end
306

    
307
    alias ssl? tls?
308

    
309
    # Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for
310
    # this object.  Must be called before the connection is established
311
    # to have any effect.  +context+ is a OpenSSL::SSL::SSLContext object.
312
    def enable_tls(context = SMTP.default_ssl_context)
313
      raise 'openssl library not installed' unless defined?(OpenSSL)
314
      raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls
315
      @tls = true
316
      @ssl_context = context
317
    end
318

    
319
    alias enable_ssl enable_tls
320
    
321
    # Disables SMTP/TLS for this object.  Must be called before the
322
    # connection is established to have any effect.
323
    def disable_tls
324
      @tls = false
325
      @ssl_context = nil
326
    end
327

    
328
    alias disable_ssl disable_tls
329

    
330
    # Returns truth value if this object uses STARTTLS.
331
    # If this object always uses STARTTLS, returns :always.
332
    # If this object uses STARTTLS when the server support TLS, returns :auto.
333
    def starttls?
334
      @starttls
335
    end
336

    
337
    # true if this object uses STARTTLS.
338
    def starttls_always?
339
      @starttls == :always
340
    end
341

    
342
    # true if this object uses STARTTLS when server advertises STARTTLS.
343
    def starttls_auto?
344
      @starttls == :auto
345
    end
346
    
347
    # Enables SMTP/TLS (STARTTLS) for this object.
348
    # +context+ is a OpenSSL::SSL::SSLContext object.
349
    def enable_starttls(context = SMTP.default_ssl_context)
350
      raise 'openssl library not installed' unless defined?(OpenSSL)
351
      raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
352
      @starttls = :always
353
      @ssl_context = context
354
    end
355

    
356
    # Enables SMTP/TLS (STARTTLS) for this object if server accepts.
357
    # +context+ is a OpenSSL::SSL::SSLContext object.
358
    def enable_starttls_auto(context = SMTP.default_ssl_context)
359
      raise 'openssl library not installed' unless defined?(OpenSSL)
360
      raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
361
      @starttls = :auto
362
      @ssl_context = context
363
    end
364

    
365
    # Disables SMTP/TLS (STARTTLS) for this object.  Must be called
366
    # before the connection is established to have any effect.
367
    def disable_starttls
368
      @starttls = false
369
      @ssl_context = nil
370
    end
371

    
372
    # The address of the SMTP server to connect to.
373
    attr_reader :address
374

    
375
    # The port number of the SMTP server to connect to.
376
    attr_reader :port
377

    
378
    # Seconds to wait while attempting to open a connection.
379
    # If the connection cannot be opened within this time, a
380
    # TimeoutError is raised.
381
    attr_accessor :open_timeout
382

    
383
    # Seconds to wait while reading one block (by one read(2) call).
384
    # If the read(2) call does not complete within this time, a
385
    # TimeoutError is raised.
386
    attr_reader :read_timeout
387

    
388
    # Set the number of seconds to wait until timing-out a read(2)
389
    # call.
390
    def read_timeout=(sec)
391
      @socket.read_timeout = sec if @socket
392
      @read_timeout = sec
393
    end
394

    
395
    #
396
    # WARNING: This method causes serious security holes.
397
    # Use this method for only debugging.
398
    #
399
    # Set an output stream for debug logging.
400
    # You must call this before #start.
401
    #
402
    #   # example
403
    #   smtp = Net::SMTP.new(addr, port)
404
    #   smtp.set_debug_output $stderr
405
    #   smtp.start do |smtp|
406
    #     ....
407
    #   end
408
    #
409
    def debug_output=(arg)
410
      @debug_output = arg
411
    end
412

    
413
    alias set_debug_output debug_output=
414

    
415
    #
416
    # SMTP session control
417
    #
418

    
419
    #
420
    # Creates a new Net::SMTP object and connects to the server.
421
    #
422
    # This method is equivalent to:
423
    # 
424
    #   Net::SMTP.new(address, port).start(helo_domain, account, password, authtype)
425
    #
426
    # === Example
427
    #
428
    #     Net::SMTP.start('your.smtp.server') do |smtp|
429
    #       smtp.send_message msgstr, 'from@example.com', ['dest@example.com']
430
    #     end
431
    #
432
    # === Block Usage
433
    #
434
    # If called with a block, the newly-opened Net::SMTP object is yielded
435
    # to the block, and automatically closed when the block finishes.  If called
436
    # without a block, the newly-opened Net::SMTP object is returned to
437
    # the caller, and it is the caller's responsibility to close it when
438
    # finished.
439
    #
440
    # === Parameters
441
    #
442
    # +address+ is the hostname or ip address of your smtp server.
443
    #
444
    # +port+ is the port to connect to; it defaults to port 25.
445
    #
446
    # +helo+ is the _HELO_ _domain_ provided by the client to the
447
    # server (see overview comments); it defaults to 'localhost.localdomain'. 
448
    #
449
    # The remaining arguments are used for SMTP authentication, if required
450
    # or desired.  +user+ is the account name; +secret+ is your password
451
    # or other authentication token; and +authtype+ is the authentication
452
    # type, one of :plain, :login, or :cram_md5.  See the discussion of
453
    # SMTP Authentication in the overview notes.
454
    #
455
    # === Errors
456
    #
457
    # This method may raise:
458
    #
459
    # * Net::SMTPAuthenticationError
460
    # * Net::SMTPServerBusy
461
    # * Net::SMTPSyntaxError
462
    # * Net::SMTPFatalError
463
    # * Net::SMTPUnknownError
464
    # * IOError
465
    # * TimeoutError
466
    #
467
    def SMTP.start(address, port = nil, helo = 'localhost.localdomain',
468
                   user = nil, secret = nil, authtype = nil,
469
                   &block)   # :yield: smtp
470
      new(address, port).start(helo, user, secret, authtype, &block)
471
    end
472

    
473
    # +true+ if the SMTP session has been started.
474
    def started?
475
      @started
476
    end
477

    
478
    #
479
    # Opens a TCP connection and starts the SMTP session.
480
    #
481
    # === Parameters
482
    #
483
    # +helo+ is the _HELO_ _domain_ that you'll dispatch mails from; see
484
    # the discussion in the overview notes.
485
    #
486
    # If both of +user+ and +secret+ are given, SMTP authentication 
487
    # will be attempted using the AUTH command.  +authtype+ specifies 
488
    # the type of authentication to attempt; it must be one of
489
    # :login, :plain, and :cram_md5.  See the notes on SMTP Authentication
490
    # in the overview. 
491
    #
492
    # === Block Usage
493
    #
494
    # When this methods is called with a block, the newly-started SMTP
495
    # object is yielded to the block, and automatically closed after
496
    # the block call finishes.  Otherwise, it is the caller's 
497
    # responsibility to close the session when finished.
498
    #
499
    # === Example
500
    #
501
    # This is very similar to the class method SMTP.start.
502
    #
503
    #     require 'net/smtp' 
504
    #     smtp = Net::SMTP.new('smtp.mail.server', 25)
505
    #     smtp.start(helo_domain, account, password, authtype) do |smtp|
506
    #       smtp.send_message msgstr, 'from@example.com', ['dest@example.com']
507
    #     end 
508
    #
509
    # The primary use of this method (as opposed to SMTP.start)
510
    # is probably to set debugging (#set_debug_output) or ESMTP
511
    # (#esmtp=), which must be done before the session is
512
    # started.  
513
    #
514
    # === Errors
515
    #
516
    # If session has already been started, an IOError will be raised.
517
    #
518
    # This method may raise:
519
    #
520
    # * Net::SMTPAuthenticationError
521
    # * Net::SMTPServerBusy
522
    # * Net::SMTPSyntaxError
523
    # * Net::SMTPFatalError
524
    # * Net::SMTPUnknownError
525
    # * IOError
526
    # * TimeoutError
527
    #
528
    def start(helo = 'localhost.localdomain',
529
              user = nil, secret = nil, authtype = nil)   # :yield: smtp
530
      if block_given?
531
        begin
532
          do_start helo, user, secret, authtype
533
          return yield(self)
534
        ensure
535
          do_finish
536
        end
537
      else
538
        do_start helo, user, secret, authtype
539
        return self
540
      end
541
    end
542

    
543
    # Finishes the SMTP session and closes TCP connection.
544
    # Raises IOError if not started.
545
    def finish
546
      raise IOError, 'not yet started' unless started?
547
      do_finish
548
    end
549

    
550
    private
551

    
552
    def do_start(helo_domain, user, secret, authtype)
553
      raise IOError, 'SMTP session already started' if @started
554
      if user or secret
555
        check_auth_method(authtype || DEFAULT_AUTH_TYPE)
556
        check_auth_args user, secret
557
      end
558
      s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }   
559
      logging "Connection opened: #{@address}:#{@port}"
560
      @socket = new_internet_message_io(tls? ? tlsconnect(s) : s)
561
      check_response critical { recv_response() }
562
      do_helo helo_domain
563
      if starttls_always? or (capable_starttls? and starttls_auto?)
564
        unless capable_starttls?
565
          raise SMTPUnsupportedCommand,
566
              "STARTTLS is not supported on this server"
567
        end
568
        starttls
569
        @socket = new_internet_message_io(tlsconnect(s))
570
        # helo response may be different after STARTTLS
571
        do_helo helo_domain
572
      end
573
      authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user
574
      @started = true
575
    ensure
576
      unless @started
577
        # authentication failed, cancel connection.
578
        s.close if s and not s.closed?
579
        @socket = nil
580
      end
581
    end
582

    
583
    def tlsconnect(s)
584
      s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
585
      logging "TLS connection started"
586
      s.sync_close = true
587
      s.connect
588
      if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
589
        s.post_connection_check(@address)
590
      end
591
      s
592
    end
593

    
594
    def new_internet_message_io(s)
595
      io = InternetMessageIO.new(s)
596
      io.read_timeout = @read_timeout
597
      io.debug_output = @debug_output
598
      io
599
    end
600

    
601
    def do_helo(helo_domain)
602
      res = @esmtp ? ehlo(helo_domain) : helo(helo_domain)
603
      @capabilities = res.capabilities
604
    rescue SMTPError
605
      if @esmtp
606
        @esmtp = false
607
        @error_occured = false
608
        retry
609
      end
610
      raise
611
    end
612

    
613
    def do_finish
614
      quit if @socket and not @socket.closed? and not @error_occured
615
    ensure
616
      @started = false
617
      @error_occured = false
618
      @socket.close if @socket and not @socket.closed?
619
      @socket = nil
620
    end
621

    
622
    #
623
    # Message Sending
624
    #
625

    
626
    public
627

    
628
    #
629
    # Sends +msgstr+ as a message.  Single CR ("\r") and LF ("\n") found
630
    # in the +msgstr+, are converted into the CR LF pair.  You cannot send a
631
    # binary message with this method. +msgstr+ should include both 
632
    # the message headers and body.
633
    #
634
    # +from_addr+ is a String representing the source mail address.
635
    #
636
    # +to_addr+ is a String or Strings or Array of Strings, representing
637
    # the destination mail address or addresses.
638
    #
639
    # === Example
640
    #
641
    #     Net::SMTP.start('smtp.example.com') do |smtp|
642
    #       smtp.send_message msgstr,
643
    #                         'from@example.com',
644
    #                         ['dest@example.com', 'dest2@example.com']
645
    #     end
646
    #
647
    # === Errors
648
    #
649
    # This method may raise:
650
    #
651
    # * Net::SMTPServerBusy
652
    # * Net::SMTPSyntaxError
653
    # * Net::SMTPFatalError
654
    # * Net::SMTPUnknownError
655
    # * IOError
656
    # * TimeoutError
657
    #
658
    def send_message(msgstr, from_addr, *to_addrs)
659
      raise IOError, 'closed session' unless @socket
660
      mailfrom from_addr
661
      rcptto_list(to_addrs) {data msgstr}
662
    end
663

    
664
    alias send_mail send_message
665
    alias sendmail send_message   # obsolete
666

    
667
    #
668
    # Opens a message writer stream and gives it to the block.
669
    # The stream is valid only in the block, and has these methods:
670
    #
671
    # puts(str = '')::       outputs STR and CR LF.
672
    # print(str)::           outputs STR.
673
    # printf(fmt, *args)::   outputs sprintf(fmt,*args).
674
    # write(str)::           outputs STR and returns the length of written bytes.
675
    # <<(str)::              outputs STR and returns self.
676
    #
677
    # If a single CR ("\r") or LF ("\n") is found in the message,
678
    # it is converted to the CR LF pair.  You cannot send a binary
679
    # message with this method.
680
    #
681
    # === Parameters
682
    #
683
    # +from_addr+ is a String representing the source mail address.
684
    #
685
    # +to_addr+ is a String or Strings or Array of Strings, representing
686
    # the destination mail address or addresses.
687
    #
688
    # === Example
689
    #
690
    #     Net::SMTP.start('smtp.example.com', 25) do |smtp|
691
    #       smtp.open_message_stream('from@example.com', ['dest@example.com']) do |f|
692
    #         f.puts 'From: from@example.com'
693
    #         f.puts 'To: dest@example.com'
694
    #         f.puts 'Subject: test message'
695
    #         f.puts
696
    #         f.puts 'This is a test message.'
697
    #       end
698
    #     end
699
    #
700
    # === Errors
701
    #
702
    # This method may raise:
703
    #
704
    # * Net::SMTPServerBusy
705
    # * Net::SMTPSyntaxError
706
    # * Net::SMTPFatalError
707
    # * Net::SMTPUnknownError
708
    # * IOError
709
    # * TimeoutError
710
    #
711
    def open_message_stream(from_addr, *to_addrs, &block)   # :yield: stream
712
      raise IOError, 'closed session' unless @socket
713
      mailfrom from_addr
714
      rcptto_list(to_addrs) {data(&block)}
715
    end
716

    
717
    alias ready open_message_stream   # obsolete
718

    
719
    #
720
    # Authentication
721
    #
722

    
723
    public
724

    
725
    DEFAULT_AUTH_TYPE = :plain
726

    
727
    def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE)
728
      check_auth_method authtype
729
      check_auth_args user, secret
730
      send auth_method(authtype), user, secret
731
    end
732

    
733
    def auth_plain(user, secret)
734
      check_auth_args user, secret
735
      res = critical {
736
        get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}"))
737
      }
738
      check_auth_response res
739
      res
740
    end
741

    
742
    def auth_ntlm(user, secret)
743
      check_auth_args user, secret
744
      res = critical {
745
        # send type1 message
746
        t1 = Net::NTLM::Message::Type1.new()
747
        t1.domain = user[/^[^\\]+/]
748
        @socket.writeline "AUTH NTLM " + t1.encode64
749

    
750
        # receive type2 message
751
        line = @socket.readline
752
	unless /334 (.+)/ =~ line
753
          raise RuntimeError, "SMTP AUTH don't recognize this: #{line}"
754
	end
755
        t2 = Net::NTLM::Message.decode64($1)
756

    
757
        # send Type3 Message
758
	t3 = t2.response({:user => user[/[^\\]+$/], :password => secret}, {:ntlmv2 => true})
759
        get_response(t3.encode64)
760
      }
761
      check_auth_response res
762
      res
763
    end
764

    
765
    def auth_login(user, secret)
766
      check_auth_args user, secret
767
      res = critical {
768
        check_auth_continue get_response('AUTH LOGIN')
769
        check_auth_continue get_response(base64_encode(user))
770
        get_response(base64_encode(secret))
771
      }
772
      check_auth_response res
773
      res
774
    end
775

    
776
    def auth_cram_md5(user, secret)
777
      check_auth_args user, secret
778
      res = critical {
779
        res0 = get_response('AUTH CRAM-MD5')
780
        check_auth_continue res0
781
        crammed = cram_md5_response(secret, res0.cram_md5_challenge)
782
        get_response(base64_encode("#{user} #{crammed}"))
783
      }
784
      check_auth_response res
785
      res
786
    end
787

    
788
    private
789

    
790
    def check_auth_method(type)
791
      unless respond_to?(auth_method(type), true)
792
        raise ArgumentError, "wrong authentication type #{type}"
793
      end
794
    end
795

    
796
    def auth_method(type)
797
      "auth_#{type.to_s.downcase}".intern
798
    end
799

    
800
    def check_auth_args(user, secret)
801
      unless user
802
        raise ArgumentError, 'SMTP-AUTH requested but missing user name'
803
      end
804
      unless secret
805
        raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase'
806
      end
807
    end
808

    
809
    def base64_encode(str)
810
      # expects "str" may not become too long
811
      [str].pack('m').gsub(/\s+/, '')
812
    end
813

    
814
    IMASK = 0x36
815
    OMASK = 0x5c
816

    
817
    # CRAM-MD5: [RFC2195]
818
    def cram_md5_response(secret, challenge)
819
      tmp = Digest::MD5.digest(cram_secret(secret, IMASK) + challenge)
820
      Digest::MD5.hexdigest(cram_secret(secret, OMASK) + tmp)
821
    end
822

    
823
    CRAM_BUFSIZE = 64
824

    
825
    def cram_secret(secret, mask)
826
      secret = Digest::MD5.digest(secret) if secret.size > CRAM_BUFSIZE
827
      buf = secret.ljust(CRAM_BUFSIZE, "\0")
828
      0.upto(buf.size - 1) do |i|
829
        buf[i] = (buf[i].ord ^ mask).chr
830
      end
831
      buf
832
    end
833

    
834
    #
835
    # SMTP command dispatcher
836
    #
837

    
838
    public
839

    
840
    def starttls
841
      getok('STARTTLS')
842
    end
843

    
844
    def helo(domain)
845
      getok("HELO #{domain}")
846
    end
847

    
848
    def ehlo(domain)
849
      getok("EHLO #{domain}")
850
    end
851

    
852
    def mailfrom(from_addr)
853
      if $SAFE > 0
854
        raise SecurityError, 'tainted from_addr' if from_addr.tainted?
855
      end
856
      getok("MAIL FROM:<#{from_addr}>")
857
    end
858

    
859
    def rcptto_list(to_addrs)
860
      raise ArgumentError, 'mail destination not given' if to_addrs.empty?
861
      ok_users = []
862
      unknown_users = []
863
      to_addrs.flatten.each do |addr|
864
        begin
865
          rcptto addr
866
        rescue SMTPAuthenticationError
867
          unknown_users << addr.dump
868
        else
869
          ok_users << addr
870
        end
871
      end
872
      raise ArgumentError, 'mail destination not given' if ok_users.empty?
873
      ret = yield
874
      unless unknown_users.empty?
875
        raise SMTPAuthenticationError, "failed to deliver for #{unknown_users.join(', ')}"
876
      end
877
      ret
878
    end
879

    
880
    def rcptto(to_addr)
881
      if $SAFE > 0
882
        raise SecurityError, 'tainted to_addr' if to_addr.tainted?
883
      end
884
      getok("RCPT TO:<#{to_addr}>")
885
    end
886

    
887
    # This method sends a message.
888
    # If +msgstr+ is given, sends it as a message.
889
    # If block is given, yield a message writer stream.
890
    # You must write message before the block is closed.
891
    #
892
    #   # Example 1 (by string)
893
    #   smtp.data(<<EndMessage)
894
    #   From: john@example.com
895
    #   To: betty@example.com
896
    #   Subject: I found a bug
897
    #   
898
    #   Check vm.c:58879.
899
    #   EndMessage
900
    #
901
    #   # Example 2 (by block)
902
    #   smtp.data {|f|
903
    #     f.puts "From: john@example.com"
904
    #     f.puts "To: betty@example.com"
905
    #     f.puts "Subject: I found a bug"
906
    #     f.puts ""
907
    #     f.puts "Check vm.c:58879."
908
    #   }
909
    #
910
    def data(msgstr = nil, &block)   #:yield: stream
911
      if msgstr and block
912
        raise ArgumentError, "message and block are exclusive"
913
      end
914
      unless msgstr or block
915
        raise ArgumentError, "message or block is required"
916
      end
917
      res = critical {
918
        check_continue get_response('DATA')
919
        if msgstr
920
          @socket.write_message msgstr
921
        else
922
          @socket.write_message_by_block(&block)
923
        end
924
        recv_response()
925
      }
926
      check_response res
927
      res
928
    end
929

    
930
    def quit
931
      getok('QUIT')
932
    end
933

    
934
    private
935

    
936
    def getok(reqline)
937
      res = critical {
938
        @socket.writeline reqline
939
        recv_response()
940
      }
941
      check_response res
942
      res
943
    end
944

    
945
    def get_response(reqline)
946
      @socket.writeline reqline
947
      recv_response()
948
    end
949

    
950
    def recv_response
951
      buf = ''
952
      while true
953
        line = @socket.readline
954
        buf << line << "\n"
955
        break unless line[3,1] == '-'   # "210-PIPELINING"
956
      end
957
      Response.parse(buf)
958
    end
959

    
960
    def critical(&block)
961
      return '200 dummy reply code' if @error_occured
962
      begin
963
        return yield()
964
      rescue Exception
965
        @error_occured = true
966
        raise
967
      end
968
    end
969

    
970
    def check_response(res)
971
      unless res.success?
972
        raise res.exception_class, res.message
973
      end
974
    end
975

    
976
    def check_continue(res)
977
      unless res.continue?
978
        raise SMTPUnknownError, "could not get 3xx (#{res.status})"
979
      end
980
    end
981

    
982
    def check_auth_response(res)
983
      unless res.success?
984
        raise SMTPAuthenticationError, res.message
985
      end
986
    end
987

    
988
    def check_auth_continue(res)
989
      unless res.continue?
990
        raise res.exception_class, res.message
991
      end
992
    end
993

    
994
    class Response
995
      def Response.parse(str)
996
        new(str[0,3], str)
997
      end
998

    
999
      def initialize(status, string)
1000
        @status = status
1001
        @string = string
1002
      end
1003

    
1004
      attr_reader :status
1005
      attr_reader :string
1006

    
1007
      def status_type_char
1008
        @status[0, 1]
1009
      end
1010

    
1011
      def success?
1012
        status_type_char() == '2'
1013
      end
1014

    
1015
      def continue?
1016
        status_type_char() == '3'
1017
      end
1018

    
1019
      def message
1020
        @string.lines.first
1021
      end
1022

    
1023
      def cram_md5_challenge
1024
        @string.split(/ /)[1].unpack('m')[0]
1025
      end
1026

    
1027
      def capabilities
1028
        return {} unless @string[3, 1] == '-'
1029
        h = {}
1030
        @string.lines.drop(1).each do |line|
1031
          k, *v = line[4..-1].chomp.split(nil)
1032
          h[k] = v
1033
        end
1034
        h
1035
      end
1036

    
1037
      def exception_class
1038
        case @status
1039
        when /\A4/  then SMTPServerBusy
1040
        when /\A50/ then SMTPSyntaxError
1041
        when /\A53/ then SMTPAuthenticationError
1042
        when /\A5/  then SMTPFatalError
1043
        else             SMTPUnknownError
1044
        end
1045
      end
1046
    end
1047

    
1048
    def logging(msg)
1049
      @debug_output << msg + "\n" if @debug_output
1050
    end
1051

    
1052
  end   # class SMTP
1053

    
1054
  SMTPSession = SMTP
1055

    
1056
end
(2-2/2)