lib/net/smtp.rb in net-smtp-0.3.4 vs lib/net/smtp.rb in net-smtp-0.4.0

- old
+ new

@@ -1,6 +1,7 @@ # frozen_string_literal: true + # = net/smtp.rb # # Copyright (c) 1999-2007 Yukihiro Matsumoto. # # Copyright (c) 1999-2007 Minero Aoki. @@ -10,27 +11,20 @@ # Documented by William Webber and Minero Aoki. # # This program is free software. You can re-distribute and/or # modify this program under the same terms as Ruby itself. # -# $Id$ -# # See Net::SMTP for documentation. # require 'net/protocol' begin require 'openssl' rescue LoadError - begin - require 'digest/md5' - rescue LoadError - end end module Net - # Module mixed in to all SMTP error classes module SMTPError # This *class* is a module for backward compatibility. # In later release, this module becomes a class. @@ -40,11 +34,11 @@ if response.is_a?(::Net::SMTP::Response) @response = response @message = message else @response = nil - @message = message || response + @message = message || response end end def message @message || response.message @@ -84,21 +78,28 @@ # # == What is This Library? # # This library provides functionality to send internet # mail via SMTP, the Simple Mail Transfer Protocol. For details of - # SMTP itself, see [RFC2821] (http://www.ietf.org/rfc/rfc2821.txt). + # SMTP itself, see [RFC5321] (http://www.ietf.org/rfc/rfc5321.txt). + # This library also implements SMTP authentication, which is often + # necessary for message composers to submit messages to their + # outgoing SMTP server, see + # [RFC6409](http://www.ietf.org/rfc/rfc6503.txt), + # and [SMTPUTF8](http://www.ietf.org/rfc/rfc6531.txt), which is + # necessary to send messages to/from addresses containing characters + # outside the ASCII range. # # == What is This Library NOT? # # This library does NOT provide functions to compose internet mails. # You must create them by yourself. If you want better mail support, # try RubyMail or TMail or search for alternatives in # {RubyGems.org}[https://rubygems.org/] or {The Ruby # Toolbox}[https://www.ruby-toolbox.com/]. # - # FYI: the official documentation on internet mail is: [RFC2822] (http://www.ietf.org/rfc/rfc2822.txt). + # FYI: the official specification on internet mail is: [RFC5322] (http://www.ietf.org/rfc/rfc5322.txt). # # == Examples # # === Sending Messages # @@ -184,14 +185,12 @@ # # CRAM MD5 # Net::SMTP.start('your.smtp.server', 25 # user: 'Your Account', secret: 'Your Password', authtype: :cram_md5) # class SMTP < Protocol - VERSION = "0.3.4" + VERSION = "0.4.0" - Revision = %q$Revision$.split[1] - # The default SMTP port number, 25. def SMTP.default_port 25 end @@ -209,11 +208,11 @@ alias default_ssl_port default_tls_port end def SMTP.default_ssl_context(ssl_context_params = nil) context = OpenSSL::SSL::SSLContext.new - context.set_params(ssl_context_params ? ssl_context_params : {}) + context.set_params(ssl_context_params || {}) context end # # Creates a new Net::SMTP object. @@ -280,11 +279,11 @@ # retry (but not vice versa). # attr_accessor :esmtp # +true+ if the SMTP object uses ESMTP (which it does by default). - alias :esmtp? :esmtp + alias esmtp? esmtp # true if server advertises STARTTLS. # You cannot get valid value before opening SMTP session. def capable_starttls? capable?('STARTTLS') @@ -626,36 +625,23 @@ do_finish end private - def digest_class - @digest_class ||= if defined?(OpenSSL::Digest) - OpenSSL::Digest - elsif defined?(::Digest) - ::Digest - else - raise '"openssl" or "digest" library is required' - end - end - def tcp_socket(address, port) - begin - Socket.tcp address, port, nil, nil, connect_timeout: @open_timeout - rescue Errno::ETIMEDOUT #raise Net:OpenTimeout instead for compatibility with previous versions - raise Net::OpenTimeout, "Timeout to open TCP connection to "\ - "#{address}:#{port} (exceeds #{@open_timeout} seconds)" - end + TCPSocket.open address, port end def do_start(helo_domain, user, secret, authtype) raise IOError, 'SMTP session already started' if @started if user or secret check_auth_method(authtype || DEFAULT_AUTH_TYPE) check_auth_args user, secret end - s = tcp_socket(@address, @port) + s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do + tcp_socket(@address, @port) + end logging "Connection opened: #{@address}:#{@port}" @socket = new_internet_message_io(tls? ? tlsconnect(s, @ssl_context_tls) : s) check_response critical { recv_response() } do_helo helo_domain if ! tls? and (starttls_always? or (capable_starttls? and starttls_auto?)) @@ -718,10 +704,22 @@ @error_occurred = false @socket.close if @socket @socket = nil end + def requires_smtputf8(address) + if address.kind_of? Address + !address.address.ascii_only? + else + !address.ascii_only? + end + end + + def any_require_smtputf8(addresses) + addresses.any?{ |a| requires_smtputf8(a) } + end + # # Message Sending # public @@ -761,11 +759,13 @@ # * Net::SMTPUnknownError # * Net::ReadTimeout # * IOError # def send_message(msgstr, from_addr, *to_addrs) + to_addrs.flatten! raise IOError, 'closed session' unless @socket + from_addr = Address.new(from_addr, 'SMTPUTF8') if any_require_smtputf8(to_addrs) && capable?('SMTPUTF8') mailfrom from_addr rcptto_list(to_addrs) {data msgstr} end alias send_mail send_message @@ -814,67 +814,36 @@ # * Net::SMTPUnknownError # * Net::ReadTimeout # * IOError # def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream + to_addrs.flatten! raise IOError, 'closed session' unless @socket + from_addr = Address.new(from_addr, 'SMTPUTF8') if any_require_smtputf8(to_addrs) && capable?('SMTPUTF8') mailfrom from_addr rcptto_list(to_addrs) {data(&block)} end alias ready open_message_stream # obsolete # # Authentication # - public - DEFAULT_AUTH_TYPE = :plain def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE) check_auth_method authtype check_auth_args user, secret - public_send auth_method(authtype), user, secret + authenticator = Authenticator.auth_class(authtype).new(self) + authenticator.auth(user, secret) end - def auth_plain(user, secret) - check_auth_args user, secret - res = critical { - get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}")) - } - check_auth_response res - res - end - - def auth_login(user, secret) - check_auth_args user, secret - res = critical { - check_auth_continue get_response('AUTH LOGIN') - check_auth_continue get_response(base64_encode(user)) - get_response(base64_encode(secret)) - } - check_auth_response res - res - end - - def auth_cram_md5(user, secret) - check_auth_args user, secret - res = critical { - res0 = get_response('AUTH CRAM-MD5') - check_auth_continue res0 - crammed = cram_md5_response(secret, res0.cram_md5_challenge) - get_response(base64_encode("#{user} #{crammed}")) - } - check_auth_response res - res - end - private def check_auth_method(type) - unless respond_to?(auth_method(type), true) + unless Authenticator.auth_class(type) raise ArgumentError, "wrong authentication type #{type}" end end def auth_method(type) @@ -888,35 +857,10 @@ unless secret raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase' end end - def base64_encode(str) - # expects "str" may not become too long - [str].pack('m0') - end - - IMASK = 0x36 - OMASK = 0x5c - - # CRAM-MD5: [RFC2195] - def cram_md5_response(secret, challenge) - tmp = digest_class::MD5.digest(cram_secret(secret, IMASK) + challenge) - digest_class::MD5.hexdigest(cram_secret(secret, OMASK) + tmp) - end - - CRAM_BUFSIZE = 64 - - def cram_secret(secret, mask) - secret = digest_class::MD5.digest(secret) if secret.size > CRAM_BUFSIZE - buf = secret.ljust(CRAM_BUFSIZE, "\0") - 0.upto(buf.size - 1) do |i| - buf[i] = (buf[i].ord ^ mask).chr - end - buf - end - # # SMTP command dispatcher # public @@ -939,33 +883,24 @@ getok("EHLO #{domain}") end # +from_addr+ is +String+ or +Net::SMTP::Address+ def mailfrom(from_addr) - addr = Address.new(from_addr) + addr = if requires_smtputf8(from_addr) && capable?("SMTPUTF8") + Address.new(from_addr, "SMTPUTF8") + else + Address.new(from_addr) + end getok((["MAIL FROM:<#{addr.address}>"] + addr.parameters).join(' ')) end def rcptto_list(to_addrs) raise ArgumentError, 'mail destination not given' if to_addrs.empty? - ok_users = [] - unknown_users = [] to_addrs.flatten.each do |addr| - begin - rcptto addr - rescue SMTPAuthenticationError - unknown_users << addr.to_s.dump - else - ok_users << addr - end + rcptto addr end - raise ArgumentError, 'mail destination not given' if ok_users.empty? - ret = yield - unless unknown_users.empty? - raise SMTPAuthenticationError, "failed to deliver for #{unknown_users.join(', ')}" - end - ret + yield end # +to_addr+ is +String+ or +Net::SMTP::Address+ def rcptto(to_addr) addr = Address.new(to_addr) @@ -1024,10 +959,16 @@ def quit getok('QUIT') end + def get_response(reqline) + validate_line reqline + @socket.writeline reqline + recv_response() + end + private def validate_line(line) # A bare CR or LF is not allowed in RFC5321. if /[\r\n]/ =~ line @@ -1043,16 +984,10 @@ } check_response res res end - def get_response(reqline) - validate_line reqline - @socket.writeline reqline - recv_response() - end - def recv_response buf = ''.dup while true line = @socket.readline buf << line << "\n" @@ -1081,22 +1016,10 @@ unless res.continue? raise SMTPUnknownError.new(res, message: "could not get 3xx (#{res.status}: #{res.string})") end end - def check_auth_response(res) - unless res.success? - raise SMTPAuthenticationError.new(res) - end - end - - def check_auth_continue(res) - unless res.continue? - raise res.exception_class.new(res) - end - end - # This class represents a response received by the SMTP server. Instances # of this class are created by the SMTP class; they should not be directly # created by the user. For more information on SMTP responses, view # {Section 4.2 of RFC 5321}[http://tools.ietf.org/html/rfc5321#section-4.2] class Response @@ -1194,19 +1117,23 @@ if address.kind_of? Address @address = address.address @parameters = address.parameters else @address = address - @parameters = (args + [kw_args]).map{|param| Array(param)}.flatten(1).map{|param| Array(param).compact.join('=')} + @parameters = [] end + @parameters = (parameters + args + [kw_args]).map{|param| Array(param)}.flatten(1).map{|param| Array(param).compact.join('=')}.uniq end def to_s @address end end - end # class SMTP SMTPSession = SMTP # :nodoc: +end +require_relative 'smtp/authenticator' +Dir.glob("#{__dir__}/smtp/auth_*.rb") do |r| + require_relative r end