Skip to content
Closed
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
123 changes: 123 additions & 0 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,30 @@ def capability
end
end

# Sends an ID command, and returns a hash of the server's
# response, or nil if the server does not identify itself.
#
# Note that the user should first check if the server supports the ID
# capability. For example:
#
# capabilities = imap.capability
# if capabilities.include?("ID")
# id = imap.id(
# name: "my IMAP client (ruby)",
# version: MyIMAP::VERSION,
# "support-url": "mailto:[email protected]",
# os: RbConfig::CONFIG["host_os"],
# )
# end
#
# See RFC 2971, Section 3.3, for defined fields.
def id(client_id=nil)
synchronize do
send_command("ID", ClientID.new(client_id))
@responses.delete("ID")&.last
end
end

# Sends a NOOP command to the server. It does nothing.
def noop
send_command("NOOP")
Expand Down Expand Up @@ -1656,6 +1680,74 @@ def validate_internal(data)
end
end

class ClientID # :nodoc:

def send_data(imap, tag)
imap.__send__(:send_data, format_internal(@data), tag)
end

def validate
validate_internal(@data)
end

private

def initialize(data)
@data = data
end

def validate_internal(client_id)
client_id.to_h.each do |k,v|
unless StringFormatter.valid_string?(k)
raise DataFormatError, client_id.inspect
end
end
rescue NoMethodError, TypeError # to_h failed
raise DataFormatError, client_id.inspect
end

def format_internal(client_id)
return nil if client_id.nil?
client_id.to_h.flat_map {|k,v|
[StringFormatter.string(k), StringFormatter.nstring(v)]
}
end

end

module StringFormatter

LITERAL_REGEX = /[\x80-\xff\r\n]/n

module_function

# Allows symbols in addition to strings
def valid_string?(str)
str.is_a?(Symbol) || str.respond_to?(:to_str)
end

# Allows nil, symbols, and strings
def valid_nstring?(str)
str.nil? || valid_string?(str)
end

# coerces using +to_s+
def string(str)
str = str.to_s
if str =~ LITERAL_REGEX
Literal.new(str)
else
QuotedString.new(str)
end
end

# coerces non-nil using +to_s+
def nstring(str)
str.nil? ? nil : string(str)
end

end

# Common validators of number and nz_number types
module NumValidator # :nodoc
class << self
Expand Down Expand Up @@ -2291,6 +2383,8 @@ def response_untagged
return response_cond
when /\A(?:FLAGS)\z/ni
return flags_response
when /\A(?:ID)\z/ni
return id_response
when /\A(?:LIST|LSUB|XLIST)\z/ni
return list_response
when /\A(?:QUOTA)\z/ni
Expand Down Expand Up @@ -3129,6 +3223,35 @@ def capability_response
return UntaggedResponse.new(name, data, @str)
end

def id_response
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
token = match(T_LPAR, T_NIL)
if token.symbol == T_NIL
return UntaggedResponse.new(name, nil, @str)
else
data = {}
while true
token = lookahead
case token.symbol
when T_RPAR
shift_token
break
when T_SPACE
shift_token
next
else
key = string
match(T_SPACE)
val = nstring
data[key] = val
end
end
return UntaggedResponse.new(name, data, @str)
end
end

def resp_text
@lex_state = EXPR_RTEXT
token = lookahead
Expand Down
49 changes: 49 additions & 0 deletions test/net/imap/test_imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,55 @@ def test_append_fail
end
end

def test_id
server = create_tcp_server
port = server.addr[1]
requests = Queue.new
server_id = {"name" => "test server", "version" => "v0.1.0"}
server_id_str = '("name" "test server" "version" "v0.1.0")'
@threads << Thread.start do
sock = server.accept
begin
sock.print("* OK test server\r\n")
requests.push(sock.gets)
# RFC 2971 very clearly states (in section 3.2):
# "a server MUST send a tagged ID response to an ID command."
# And yet... some servers report ID capability but won't the response.
sock.print("RUBY0001 OK ID completed\r\n")
requests.push(sock.gets)
sock.print("* ID #{server_id_str}\r\n")
sock.print("RUBY0002 OK ID completed\r\n")
requests.push(sock.gets)
sock.print("* ID #{server_id_str}\r\n")
sock.print("RUBY0003 OK ID completed\r\n")
requests.push(sock.gets)
sock.print("* BYE terminating connection\r\n")
sock.print("RUBY0004 OK LOGOUT completed\r\n")
ensure
sock.close
server.close
end
end

begin
imap = Net::IMAP.new(server_addr, :port => port)
resp = imap.id
assert_equal(nil, resp)
assert_equal("RUBY0001 ID NIL\r\n", requests.pop)
resp = imap.id({})
assert_equal(server_id, resp)
assert_equal("RUBY0002 ID ()\r\n", requests.pop)
resp = imap.id("name" => "test client", "version" => "latest")
assert_equal(server_id, resp)
assert_equal("RUBY0003 ID (\"name\" \"test client\" \"version\" \"latest\")\r\n",
requests.pop)
imap.logout
assert_equal("RUBY0004 LOGOUT\r\n", requests.pop)
ensure
imap.disconnect if imap
end
end

private

def imaps_test
Expand Down
12 changes: 12 additions & 0 deletions test/net/imap/test_imap_response_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,18 @@ def test_capability
assert_equal("AUTH=PLAIN", response.data.last)
end

def test_id
parser = Net::IMAP::ResponseParser.new
response = parser.parse("* ID NIL\r\n")
assert_equal("ID", response.name)
assert_equal(nil, response.data)
response = parser.parse("* ID (\"name\" \"GImap\" \"vendor\" \"Google, Inc.\" \"support-url\" NIL)\r\n")
assert_equal("ID", response.name)
assert_equal("GImap", response.data["name"])
assert_equal("Google, Inc.", response.data["vendor"])
assert_equal(nil, response.data.fetch("support-url"))
end

def test_mixed_boundary
parser = Net::IMAP::ResponseParser.new
response = parser.parse("* 2688 FETCH (UID 179161 BODYSTRUCTURE (" \
Expand Down