diff --git a/lib/net/pop.rb b/lib/net/pop.rb index e18bdab..0c922d0 100644 --- a/lib/net/pop.rb +++ b/lib/net/pop.rb @@ -24,6 +24,7 @@ require 'net/protocol' require 'digest/md5' require 'timeout' +require 'base64' begin require "openssl" @@ -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, @@ -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: @@ -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 @@ -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 @@ -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. @@ -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. @@ -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. @@ -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 @@ -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? @@ -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 @@ -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. @@ -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' @@ -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 diff --git a/test/net/pop/test_pop.rb b/test/net/pop/test_pop.rb index f4c807a..ca43912 100644 --- a/test/net/pop/test_pop.rb +++ b/test/net/pop/test_pop.rb @@ -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 @@ -21,7 +25,8 @@ 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 @@ -29,8 +34,8 @@ def test_pop_auth_ng 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 @@ -38,8 +43,8 @@ def test_apop_ok 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 @@ -47,8 +52,8 @@ def test_apop_ng 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 @@ -56,14 +61,33 @@ def test_apop_invalid 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 @@ -93,14 +117,14 @@ 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 @@ -108,7 +132,7 @@ def pop_test(apop=false) 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 @@ -125,7 +149,9 @@ 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 @@ -133,6 +159,17 @@ def pop_server_loop(sock, apop) 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 @@ -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