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
40 changes: 40 additions & 0 deletions lib/net/pop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ def initialize(addr, port = nil, isapop = false)
@mails = nil
@n_mails = nil
@n_bytes = nil
@capabilities = nil
end

# Does this instance use APOP authentication?
Expand Down Expand Up @@ -691,6 +692,37 @@ def delete_all # :yield: message
end
end

# Returns truthy if the server reports support for +tag+ with +param+.
# Returns +nil+ when +tag+ is not supported. Returns +false+ when +tag+ is
# supported but +param+ is not.
#
# If no +param+ is given, an array of the capability's parameters is
# returned.
def capable?(tag, param = nil)
capability = capabilities[tag.upcase] and
param ? capability&.include?(param.upcase) : capability
end

# Returns a hash of the server's reported capabilities. The list is cached.
#
# Each key is a capability name, and each value is an array of that
# capability's parameters.
def capabilities; @capabilities ||= command.capa end

# Clears capabilities that have been remembered by the Net::POP client.
# This forces a CAPA command to be sent the next time a #capabilities query
# method is called.
def clear_cached_capabilities; @capabilities = nil end

# Returns whether the server supports the +AUTH+ command using +mechanism+.
# If +mechanism+ isn't given, returns nil when the server doesn't support
# +SASL+ and an array of supported mechanism names otherwise.
def sasl_capable?(mechanism = nil) capable?("SASL", mechanism) end

# Returns the list of SASL mechanisms supported by the server, or an empty
# array if the server does not support any SASL authentication.
def sasl_mechanisms; sasl_capable? || [] end

# Resets the session. This clears all "deleted" marks from messages.
#
# This method raises a POPError if an error occurs.
Expand Down Expand Up @@ -918,6 +950,14 @@ def apop(account, password)
})
end

def capa
critical {
getok 'CAPA'
@socket.to_enum(:each_list_item)
.to_h {|line| tag, *params = line.upcase.split; [tag, params] }
}
end

def list
critical {
getok 'LIST'
Expand Down
45 changes: 45 additions & 0 deletions test/net/pop/test_pop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,40 @@ def command.top(number, nl)
assert_not_predicate res, :frozen?
end

test "CAPA" do
pop_test do |pop|
pop.start(@ok_user, @users[@ok_user]) do |pop|
assert_equal(
{
"TOP" => [], "USER" => [], "STLS" => [], "EXPIRE" => ["60"],
"IMPLEMENTATION" => %w[BLURDYBLURP POP3 SERVER],
"SASL" => %w[PLAIN OAUTHBEARER XOAUTH DIGEST-MD5 GSSAPI ANONYMOUS],
},
pop.capabilities
)

assert_equal ["60"], pop.capable?("expire")

assert pop.capable?("TOP")
assert pop.capable?("user")
assert_same true, pop.capable?("Implementation", "POP3")

assert_same nil, pop.capable?("UTF-8")
assert_same nil, pop.capable?("UTF-8", "USER")

assert_kind_of Array, pop.sasl_capable?
assert_same true, pop.sasl_capable?("PLAIN")
assert_same false, pop.sasl_capable?("CRAM-MD5")
assert_equal %w[PLAIN OAUTHBEARER XOAUTH DIGEST-MD5 GSSAPI ANONYMOUS],
pop.sasl_mechanisms

refute pop.capable? "PIPELINING"
refute pop.capable?("Implementation", "POP4")
refute pop.sasl_capable? "CRAM-MD5"
end
end
end

def pop_test(apop=false)
host = 'localhost'
server = TCPServer.new(host, 0)
Expand Down Expand Up @@ -134,6 +168,17 @@ def pop_server_loop(sock, apop)
user = nil
while line = sock.gets
case line
when /\ACAPA\r\n\z/i
(<<~CAPA).each_line { sock.print "#{_1.chop}\r\n" }
+OK List of capabilities follows
TOP
USER
SASL PLAIN OAUTHBEARER XOAUTH DIGEST-MD5 GSSAPI ANONYMOUS
STLS
IMPLEMENTATION BlurdyBlurp POP3 server
EXPIRE 60
.
CAPA
when /^USER (.+)\r\n/
user = $1
if @users.key?(user)
Expand Down