diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb index f354708..70d6234 100644 --- a/lib/net/smtp.rb +++ b/lib/net/smtp.rb @@ -81,12 +81,23 @@ class SMTPUnsupportedCommand < ProtocolError include SMTPError end + # Represents a need to use SMTPUTF8 when the server does not support it + class SMTPUTF8RequiredError < SMTPUnsupportedCommand + end + # # == 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? # @@ -96,7 +107,7 @@ class SMTPUnsupportedCommand < ProtocolError # {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 # @@ -720,6 +731,18 @@ def do_finish @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 # @@ -758,13 +781,14 @@ def do_finish # * Net::SMTPServerBusy # * Net::SMTPSyntaxError # * Net::SMTPFatalError + # * Net::SMTPUTF8RequiredError # * Net::SMTPUnknownError # * Net::ReadTimeout # * IOError # def send_message(msgstr, from_addr, *to_addrs) raise IOError, 'closed session' unless @socket - mailfrom from_addr + mailfrom from_addr, any_require_smtputf8(to_addrs) rcptto_list(to_addrs) {data msgstr} end @@ -811,13 +835,14 @@ def send_message(msgstr, from_addr, *to_addrs) # * Net::SMTPServerBusy # * Net::SMTPSyntaxError # * Net::SMTPFatalError + # * Net::SMTPUTF8RequiredError # * Net::SMTPUnknownError # * Net::ReadTimeout # * IOError # def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream raise IOError, 'closed session' unless @socket - mailfrom from_addr + mailfrom from_addr, any_require_smtputf8(to_addrs) rcptto_list(to_addrs) {data(&block)} end @@ -940,8 +965,13 @@ def ehlo(domain) end # +from_addr+ is +String+ or +Net::SMTP::Address+ - def mailfrom(from_addr) - addr = Address.new(from_addr) + def mailfrom(from_addr, require_smtputf8 = false) + addr = if require_smtputf8 || requires_smtputf8(from_addr) + raise SMTPUTF8RequiredError, "Message requires SMTPUTF8 but server does not support that" unless capable? "SMTPUTF8" + Address.new(from_addr, "SMTPUTF8") + else + Address.new(from_addr) + end getok((["MAIL FROM:<#{addr.address}>"] + addr.parameters).join(' ')) end @@ -1196,8 +1226,9 @@ def initialize(address, *args, **kw_args) @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('=')} end def to_s diff --git a/test/net/smtp/test_smtp.rb b/test/net/smtp/test_smtp.rb index b418b3f..3706672 100644 --- a/test/net/smtp/test_smtp.rb +++ b/test/net/smtp/test_smtp.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # frozen_string_literal: true require 'net/smtp' require 'stringio' @@ -9,6 +10,16 @@ class TestSMTP < Test::Unit::TestCase SERVER_KEY = File.expand_path("../fixtures/server.key", __dir__) SERVER_CERT = File.expand_path("../fixtures/server.crt", __dir__) + class FakeIO + def sync + nil + end + def sync=(unused) + end + def flush + end + end + class FakeSocket attr_reader :write_io @@ -26,6 +37,16 @@ def readline raise 'ran out of input' unless line line.chop end + + def io + return @io ||= FakeIO.new + end + + def write_message(unused) + end + + def write_message_by_block + end end def setup @@ -76,13 +97,13 @@ def test_server_capabilities smtp = Net::SMTP.start('localhost', port, starttls: false) assert_equal({"STARTTLS"=>[], "AUTH"=>["PLAIN"]}, smtp.capabilities) assert_equal(true, smtp.capable?('STARTTLS')) - assert_equal(false, smtp.capable?('SMTPUTF8')) + assert_equal(false, smtp.capable?('DOES-NOT-EXIST')) else port = fake_server_start smtp = Net::SMTP.start('localhost', port, starttls: false) assert_equal({"AUTH"=>["PLAIN"]}, smtp.capabilities) assert_equal(false, smtp.capable?('STARTTLS')) - assert_equal(false, smtp.capable?('SMTPUTF8')) + assert_equal(false, smtp.capable?('DOES-NOT-EXIST')) end smtp.finish end @@ -560,6 +581,51 @@ def test_start_instance_invalid_number_of_arguments assert_equal('wrong number of arguments (given 5, expected 0..4)', err.message) end + def test_send_smtputf_sender_without_server + sock = FakeSocket.new("220 OK\r\n250-test\r\n250 SMTPUTF8\r\n") + smtp = Net::SMTP.new 'localhost', 25 + smtp.instance_variable_set :@socket, sock + assert_raise(Net::SMTPUTF8RequiredError) do + smtp.send_message('message', 'rené@example.com') + end + end + + def test_send_smtputf8_sender + smtp = Net::SMTP.new 'localhost', 25 + smtp.instance_variable_set :@capabilities, {"SMTPUTF8"=>[]} + sock = FakeSocket.new("250 OK\r\n250 OK\r\n354 Blah\r\n250 Queued, in a way\r\n") + smtp.instance_variable_set :@socket, sock + smtp.send_message('message', 'rené@example.com', 'foo@example.com') + assert sock.write_io.string.include? "MAIL FROM: SMTPUTF8\r\n" + end + + def test_send_smtputf8_sender_with_size + smtp = Net::SMTP.new 'localhost', 25 + smtp.instance_variable_set :@capabilities, {"SMTPUTF8"=>[]} + sock = FakeSocket.new("250 OK\r\n250 OK\r\n354 Blah\r\n250 Queued, in a way\r\n") + smtp.instance_variable_set :@socket, sock + smtp.send_message('message', Net::SMTP::Address.new('rené@example.com', 'SIZE=42'), 'foo@example.com') + assert sock.write_io.string.include? "MAIL FROM: SIZE=42 SMTPUTF8\r\n" + end + + def test_send_smtputf_recipient + smtp = Net::SMTP.new 'localhost', 25 + smtp.instance_variable_set :@capabilities, {"SMTPUTF8"=>[]} + sock = FakeSocket.new("250 MAIL OK\r\n250 RCPT OK\r\n354 Blah\r\n250 Queued, in a way\r\n") + smtp.instance_variable_set :@socket, sock + smtp.send_message('message', 'foo@example.com', 'rené@example.com') + assert sock.write_io.string.include? "MAIL FROM: SMTPUTF8\r\n" + end + + def test_mailfrom_with_smtputf_detection + sock = FakeSocket.new + smtp = Net::SMTP.new 'localhost', 25 + smtp.instance_variable_set :@socket, sock + smtp.instance_variable_set :@capabilities, {"SMTPUTF8"=>""} + smtp.mailfrom("rené@example.com") + assert sock.write_io.string.include? "MAIL FROM: SMTPUTF8\r\n" + end + private def accept(servers)