lib/net/smtp.rb


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://www.ietf.org/rfc/rfc2821.txt>)).
  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://www.ietf.org/rfc/rfc2822.txt>)).
  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