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