Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SMTPUTF8 support #49

Merged
merged 3 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions lib/net/smtp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
#
Expand All @@ -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
#
Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
70 changes: 68 additions & 2 deletions test/net/smtp/test_smtp.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# coding: utf-8
# frozen_string_literal: true
require 'net/smtp'
require 'stringio'
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:<rené@example.com> 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:<rené@example.com> 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:<foo@example.com> 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:<rené@example.com> SMTPUTF8\r\n"
end

private

def accept(servers)
Expand Down