Skip to content
Open
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
100 changes: 86 additions & 14 deletions lib/net/pop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
require 'net/protocol'
require 'digest/md5'
require 'timeout'
require 'base64'

begin
require "openssl"
Expand Down Expand Up @@ -173,6 +174,20 @@ class POPBadResponse < POPError; end
# # Rest of the code is the same.
# end
#
# === Using OAUTH2
#
# The net/pop library supports OAUTH2 authentication.
# To use OAUTH2, use the Net::OAUTH2 class instead of the Net::POP3 class.
# You can use the utility method, Net::POP3.OAUTH2(). For example:
#
# require 'net/pop'
#
# # Use OAUTH2 authentication if $isoauth2 == true
# pop = Net::POP3.OAUTH2($isoauth2).new('oauth2.example.com', 110)
# pop.start('YourAccount', 'YourToken') do |pop|
# # Rest of the code is the same.
# end
#
# === Fetch Only Selected Mail Using 'UIDL' POP Command
#
# If your POP server provides UIDL functionality,
Expand Down Expand Up @@ -239,6 +254,21 @@ def POP3.APOP(isapop)
isapop ? APOP : POP3
end

# Returns the OAUTH2 if +isoauth2+ is true; otherwise, returns
# the POP class. For example:
#
# # Example 1
# pop = Net::POP3.OAUTH2($isoauth2).new(addr, port)
#
# # Example 2
# Net::POP3.OAUTH2($isoauth2).start(addr, port) do |pop|
# ....
# end
#
def POP3.OAUTH2(isoauth2)
isoauth2 ? OAUTH2 : POP3
end

# Starts a POP3 session and iterates over each POPMail object,
# yielding it to the +block+.
# This method is equivalent to:
Expand All @@ -261,8 +291,8 @@ def POP3.APOP(isapop)
#
def POP3.foreach(address, port = nil,
account = nil, password = nil,
isapop = false, &block) # :yields: message
start(address, port, account, password, isapop) {|pop|
isapop = false, isoauth2 = false, &block) # :yields: message
start(address, port, account, password, isapop, isoauth2) {|pop|
pop.each_mail(&block)
}
end
Expand All @@ -282,8 +312,8 @@ def POP3.foreach(address, port = nil,
#
def POP3.delete_all(address, port = nil,
account = nil, password = nil,
isapop = false, &block)
start(address, port, account, password, isapop) {|pop|
isapop = false, isoauth2 = false, &block)
start(address, port, account, password, isapop, isoauth2) {|pop|
pop.delete_all(&block)
}
end
Expand All @@ -302,10 +332,15 @@ def POP3.delete_all(address, port = nil,
# Net::POP3.auth_only('pop.example.com', 110,
# 'YourAccount', 'YourPassword', true)
#
# === Example: OAUTH2
#
# Net::POP3.auth_only('pop.example.com', 110,
# 'YourAccount', 'YourPassword', false, true)
#
def POP3.auth_only(address, port = nil,
account = nil, password = nil,
isapop = false)
new(address, port, isapop).auth_only account, password
isapop = false, isoauth2 = false)
new(address, port, isapop, isoauth2).auth_only account, password
end

# Starts a pop3 session, attempts authentication, and quits.
Expand Down Expand Up @@ -384,7 +419,7 @@ def POP3.certs

# Creates a new POP3 object and open the connection. Equivalent to
#
# Net::POP3.new(address, port, isapop).start(account, password)
# Net::POP3.new(address, port, isapop, isoauth2).start(account, password)
#
# If +block+ is provided, yields the newly-opened POP3 object to it,
# and automatically closes it at the end of the session.
Expand All @@ -400,8 +435,8 @@ def POP3.certs
#
def POP3.start(address, port = nil,
account = nil, password = nil,
isapop = false, &block) # :yield: pop
new(address, port, isapop).start(account, password, &block)
isapop = false, isoauth2 = false, &block) # :yield: pop
new(address, port, isapop, isoauth2).start(account, password, &block)
end

# Creates a new POP3 object.
Expand All @@ -410,15 +445,16 @@ def POP3.start(address, port = nil,
#
# The optional +port+ is the port to connect to.
#
# The optional +isapop+ specifies whether this connection is going
# to use APOP authentication; it defaults to +false+.
# The optional +isapop+, +isaoauth2+ specify whether this connection is going
# to use APOP or OAUTH2 authentication; it defaults to +false+.
#
# This method does *not* open the TCP connection.
def initialize(addr, port = nil, isapop = false)
def initialize(addr, port = nil, isapop = false, isoauth2 = false)
@address = addr
@ssl_params = POP3.ssl_params
@port = port
@apop = isapop
@oauth2 = isoauth2

@command = nil
@socket = nil
Expand All @@ -434,7 +470,12 @@ def initialize(addr, port = nil, isapop = false)

# Does this instance use APOP authentication?
def apop?
@apop
@apop != false
end

# Does this instance use OAUTH2 authentication?
def oauth2?
@oauth2
end

# does this instance use SSL?
Expand Down Expand Up @@ -561,11 +602,16 @@ def do_start(account, password) # :nodoc:
@socket = InternetMessageIO.new(s,
read_timeout: @read_timeout,
debug_output: @debug_output)
logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})"


auth = @apop ? 'APOP' : (@oauth2 ? 'OAUTH2' : 'POP')
logging "POP session started: #{@address}:#{@port} (#{auth})"
on_connect
@command = POP3Command.new(@socket)
if apop?
@command.apop account, password
elsif oauth2?
@command.oauth2 account, password
else
@command.auth account, password
end
Expand Down Expand Up @@ -733,8 +779,15 @@ def apop?
end
end

class OAUTH2 < POP3
def oauth2?
true
end
end

# class aliases
APOPSession = APOP
OAUTH2Session = OAUTH2

#
# This class represents a message which exists on the POP server.
Expand Down Expand Up @@ -921,6 +974,20 @@ def apop(account, password)
})
end

def oauth2(account, token)
# We first send the AUTH XOAUTH2 string to tell the server we want
# to authenticate using XOAUTH2. If it responds with + we send
# a base64 encoded string containing the email and the token.
# POP RFC: https://www.rfc-editor.org/rfc/rfc1734

sep = 1.chr # ^A character

check_response_auth(critical {
check_response_auth_xoauth2(get_response('AUTH XOAUTH2'))
get_response(Base64.strict_encode64("user=#{account}#{sep}auth=Bearer #{token}#{sep}#{sep}"))
})
end

def list
critical {
getok 'LIST'
Expand Down Expand Up @@ -1010,6 +1077,11 @@ def check_response_auth(res)
res
end

def check_response_auth_xoauth2(res)
raise POPAuthenticationError, res unless /\A\+/i =~ res
res
end

def critical
return '+OK dummy ok response' if @error_occurred
begin
Expand Down
76 changes: 61 additions & 15 deletions test/net/pop/test_pop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
require 'net/pop'
require 'test/unit'
require 'digest/md5'
require 'base64'

class TestPOP < Test::Unit::TestCase
def setup
@users = {'user' => 'pass' }
@ok_user = 'user'
@stamp_base = "#{$$}.#{Time.now.to_i}@localhost"
# base64 of a dummy xoauth2 token
@md5_oauth2 = 'dXNlcj1tYWlsQG1haWwuY29tAWF1dGg9QmVhcmVyIHJhbmRvbXRva2VuAQE='
end

def test_pop_auth_ok
pop_test(false) do |pop|
assert_instance_of Net::POP3, pop
pop_test(false, false) do |pop|
assert_equal pop.apop?, false
assert_equal pop.oauth2?, false
assert_nothing_raised do
pop.start(@ok_user, @users[@ok_user])
end
Expand All @@ -21,49 +25,69 @@ def test_pop_auth_ok

def test_pop_auth_ng
pop_test(false) do |pop|
assert_instance_of Net::POP3, pop
assert_equal pop.apop?, false
assert_equal pop.oauth2?, false
assert_raise Net::POPAuthenticationError do
pop.start(@ok_user, 'bad password')
end
end
end

def test_apop_ok
pop_test(@stamp_base) do |pop|
assert_instance_of Net::APOP, pop
pop_test(@stamp_base, false) do |pop|
assert_equal pop.apop?, true
assert_nothing_raised do
pop.start(@ok_user, @users[@ok_user])
end
end
end

def test_apop_ng
pop_test(@stamp_base) do |pop|
assert_instance_of Net::APOP, pop
pop_test(@stamp_base, false) do |pop|
assert_equal pop.apop?, true
assert_raise Net::POPAuthenticationError do
pop.start(@ok_user, 'bad password')
end
end
end

def test_apop_invalid
pop_test("\x80"+@stamp_base) do |pop|
assert_instance_of Net::APOP, pop
pop_test("\x80"+@stamp_base, false) do |pop|
assert_equal pop.apop?, true
assert_raise Net::POPAuthenticationError do
pop.start(@ok_user, @users[@ok_user])
end
end
end

def test_apop_invalid_at
pop_test(@stamp_base.sub('@', '.')) do |pop|
assert_instance_of Net::APOP, pop
pop_test(@stamp_base.sub('@', '.'), false) do |pop|
assert_equal pop.apop?, true
assert_raise Net::POPAuthenticationError do
pop.start(@ok_user, @users[@ok_user])
end
end
end

def test_oauth2
pop_test(false, true) do |pop|
assert_equal pop.oauth2?, true
assert_nothing_raised do
pop.start('mail@mail.com', 'randomtoken')
end
end
end

def test_oauth2_invalid
pop_test(false, true) do |pop|
assert_equal pop.oauth2?, true
assert_raise Net::POPAuthenticationError do
pop.start('mail@mail.com', 'wrongtoken')
end
end
end


def test_popmail
# totally not representative of real messages, but
# enough to test frozen bugs
Expand Down Expand Up @@ -93,22 +117,22 @@ def command.top(number, nl)
assert_not_predicate res, :frozen?
end

def pop_test(apop=false)
def pop_test(apop=false, oauth2=false)
host = 'localhost'
server = TCPServer.new(host, 0)
port = server.addr[1]
server_thread = Thread.start do
sock = server.accept
begin
pop_server_loop(sock, apop)
pop_server_loop(sock, apop, oauth2)
ensure
sock.close
end
end
client_thread = Thread.start do
begin
begin
pop = Net::POP3::APOP(apop).new(host, port)
pop = Net::POP3.new(host, port, apop, oauth2)
#pop.set_debug_output $stderr
yield pop
ensure
Expand All @@ -125,14 +149,27 @@ def pop_test(apop=false)
assert_join_threads([client_thread, server_thread])
end

def pop_server_loop(sock, apop)
def pop_server_loop(sock, apop, oauth2)
oauth2_auth_started = false

if apop
sock.print "+OK ready <#{apop}>\r\n"
else
sock.print "+OK ready\r\n"
end
user = nil
while line = sock.gets
if oauth2_auth_started
if line.chop == @md5_oauth2
sock.print "+OK\r\n"
else
sock.print "-ERR Authentication failure: unknown user name or bad password.\r\n"
end

oauth2_auth_started = false
next
end

case line
when /^USER (.+)\r\n/
user = $1
Expand All @@ -157,7 +194,16 @@ def pop_server_loop(sock, apop)
when /^QUIT/
sock.print "+OK bye\r\n"
return
when /^AUTH XOAUTH2\r\n/
if not oauth2
sock.print "+ERR command not recognized\r\n"
return
end

sock.print "+\r\n"
oauth2_auth_started = true
else
printf line
sock.print "-ERR command not recognized\r\n"
return
end
Expand Down