Skip to content

Commit 8e9a213

Browse files
committed
🔒 Enforce LOGINDISABLED requirement
This may be considered a "breaking change", but it should have no negative effect on well behaved servers. This should merely change a NoResponseError into a LoginDisabledError. However, some broken servers have been known to hang indefinitely when issued a `CAPABILITY` command prior to authentication. For those servers, we offer the `enforce_logindisabled` config option. Fixes #32.
1 parent d276458 commit 8e9a213

File tree

5 files changed

+168
-8
lines changed

5 files changed

+168
-8
lines changed

lib/net/imap.rb

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,20 +1378,19 @@ def authenticate(mechanism, *creds,
13781378
# ===== Capabilities
13791379
#
13801380
# An IMAP client MUST NOT call #login when the server advertises the
1381-
# +LOGINDISABLED+ capability.
1382-
#
1383-
# if imap.capability? "LOGINDISABLED"
1384-
# raise "Remote server has disabled the login command"
1385-
# else
1386-
# imap.login username, password
1387-
# end
1381+
# +LOGINDISABLED+ capability. By default, Net::IMAP will raise a
1382+
# LoginDisabledError when that capability is present. See
1383+
# Config#enforce_logindisabled.
13881384
#
13891385
# Server capabilities may change after #starttls, #login, and #authenticate.
13901386
# Cached capabilities _must_ be invalidated after this method completes.
13911387
# The TaggedResponse to #login may include updated capabilities in its
13921388
# ResponseCode.
13931389
#
13941390
def login(user, password)
1391+
if enforce_logindisabled? && capability?("LOGINDISABLED")
1392+
raise LoginDisabledError
1393+
end
13951394
send_command("LOGIN", user, password)
13961395
.tap { @capabilities = capabilities_from_resp_code _1 }
13971396
end
@@ -2869,6 +2868,14 @@ def put_string(str)
28692868
end
28702869
end
28712870

2871+
def enforce_logindisabled?
2872+
if config.enforce_logindisabled == :when_capabilities_cached
2873+
capabilities_cached?
2874+
else
2875+
config.enforce_logindisabled
2876+
end
2877+
end
2878+
28722879
def search_internal(cmd, keys, charset)
28732880
if keys.instance_of?(String)
28742881
keys = [RawData.new(keys)]

lib/net/imap/config.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,32 @@ def self.[](config)
213213
# | v0.4 | +true+ <em>(support added)</em> |
214214
attr_accessor :sasl_ir, type: :boolean
215215

216+
# :markup: markdown
217+
#
218+
# Controls the behavior of Net::IMAP#login when the `LOGINDISABLED`
219+
# capability is present. When enforced, Net::IMAP will raise a
220+
# LoginDisabledError when that capability is present. Valid values are:
221+
#
222+
# [+false+]
223+
# Send the +LOGIN+ command without checking for +LOGINDISABLED+.
224+
#
225+
# [+:when_capabilities_cached+]
226+
# Enforce the requirement when Net::IMAP#capabilities_cached? is true,
227+
# but do not send a +CAPABILITY+ command to discover the capabilities.
228+
#
229+
# [+true+]
230+
# Only send the +LOGIN+ command if the +LOGINDISABLED+ capability is not
231+
# present. When capabilities are unknown, Net::IMAP will automatically
232+
# send a +CAPABILITY+ command first before sending +LOGIN+.
233+
#
234+
# | Starting with version | The default value is |
235+
# |-------------------------|--------------------------------|
236+
# | _original_ | `false` |
237+
# | v0.5 | `true` |
238+
attr_accessor :enforce_logindisabled, type: [
239+
false, :when_capabilities_cached, true
240+
]
241+
216242
# :markup: markdown
217243
#
218244
# Controls the behavior of Net::IMAP#responses when called without a
@@ -306,6 +332,7 @@ def defaults_hash
306332
open_timeout: 30,
307333
idle_response_timeout: 5,
308334
sasl_ir: true,
335+
enforce_logindisabled: true,
309336
responses_without_block: :warn,
310337
).freeze
311338

@@ -317,6 +344,7 @@ def defaults_hash
317344
version_defaults[0] = Config[:current].dup.update(
318345
sasl_ir: false,
319346
responses_without_block: :silence_deprecation_warning,
347+
enforce_logindisabled: false,
320348
).freeze
321349
version_defaults[0.0] = Config[0]
322350
version_defaults[0.1] = Config[0]

lib/net/imap/errors.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ class IMAP < Protocol
77
class Error < StandardError
88
end
99

10+
class LoginDisabledError < Error
11+
def initialize(msg = "Remote server has disabled the LOGIN command", ...)
12+
super
13+
end
14+
end
15+
1016
# Error raised when data is in the incorrect format.
1117
class DataFormatError < Error
1218
end

test/net/imap/test_imap_capabilities.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def teardown
259259
end
260260

261261
test "#capabilities cache is NOT cleared after #login fails" do
262-
with_fake_server(preauth: false, cleartext_auth: true) do |server, imap|
262+
with_fake_server(preauth: false, cleartext_login: true) do |server, imap|
263263
original_capabilities = imap.capabilities
264264
begin
265265
imap.login("wrong_user", "wrong-password")

test/net/imap/test_imap_login.rb

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# frozen_string_literal: true
2+
3+
require "net/imap"
4+
require "test/unit"
5+
require_relative "fake_server"
6+
7+
class IMAPLoginTest < Test::Unit::TestCase
8+
include Net::IMAP::FakeServer::TestHelper
9+
10+
def setup
11+
Net::IMAP.config.reset
12+
@do_not_reverse_lookup = Socket.do_not_reverse_lookup
13+
Socket.do_not_reverse_lookup = true
14+
@threads = []
15+
end
16+
17+
def teardown
18+
if !@threads.empty?
19+
assert_join_threads(@threads)
20+
end
21+
ensure
22+
Socket.do_not_reverse_lookup = @do_not_reverse_lookup
23+
end
24+
25+
test "#login doesn't send CAPABILITY when it is already cached" do
26+
with_fake_server(
27+
preauth: false, cleartext_login: true, greeting_capabilities: true
28+
) do |server, imap|
29+
imap.login("test_user", "test-password")
30+
cmd = server.commands.pop
31+
assert_equal "LOGIN", cmd.name
32+
assert_empty server.commands
33+
end
34+
end
35+
36+
test "#login raises LoginDisabledError when LOGINDISABLED" do
37+
with_fake_server(preauth: false, cleartext_login: false) do |server, imap|
38+
assert imap.capabilities_cached?
39+
assert_raise(Net::IMAP::LoginDisabledError) do
40+
imap.login("test_user", "test-password")
41+
end
42+
assert_empty server.commands
43+
end
44+
end
45+
46+
test "#login first checks capabilities for LOGINDISABLED (success)" do
47+
with_fake_server(
48+
preauth: false, cleartext_login: true, greeting_capabilities: false
49+
) do |server, imap|
50+
imap.login("test_user", "test-password")
51+
cmd = server.commands.pop
52+
assert_equal "CAPABILITY", cmd.name
53+
cmd = server.commands.pop
54+
assert_equal "LOGIN", cmd.name
55+
assert_empty server.commands
56+
end
57+
end
58+
59+
test "#login first checks capabilities for LOGINDISABLED (failure)" do
60+
with_fake_server(
61+
preauth: false, cleartext_login: false, greeting_capabilities: false
62+
) do |server, imap|
63+
assert_raise(Net::IMAP::LoginDisabledError) do
64+
imap.login("test_user", "test-password")
65+
end
66+
cmd = server.commands.pop
67+
assert_equal "CAPABILITY", cmd.name
68+
assert_empty server.commands
69+
end
70+
end
71+
72+
test("#login sends LOGIN without asking CAPABILITY " \
73+
"when config.enforce_logindisabled is false") do
74+
with_fake_server(
75+
preauth: false, cleartext_login: false, greeting_capabilities: false
76+
) do |server, imap|
77+
imap.config.enforce_logindisabled = false
78+
imap.login("test_user", "test-password")
79+
cmd = server.commands.pop
80+
assert_equal "LOGIN", cmd.name
81+
end
82+
end
83+
84+
test("#login raises LoginDisabledError without sending CAPABILITY " \
85+
"when config.enforce_logindisabled is :when_capabilities_cached") do
86+
with_fake_server(
87+
preauth: false, cleartext_login: false, greeting_capabilities: true
88+
) do |server, imap|
89+
imap.config.enforce_logindisabled = :when_capabilities_cached
90+
assert_raise(Net::IMAP::LoginDisabledError) do
91+
imap.login("test_user", "test-password")
92+
end
93+
assert_empty server.commands
94+
end
95+
end
96+
97+
test("#login sends LOGIN without asking CAPABILITY " \
98+
"when config.enforce_logindisabled is :when_capabilities_cached") do
99+
with_fake_server(
100+
preauth: false, cleartext_login: false, greeting_capabilities: false
101+
) do |server, imap|
102+
imap.config.enforce_logindisabled = :when_capabilities_cached
103+
imap.login("test_user", "test-password")
104+
cmd = server.commands.pop
105+
assert_equal "LOGIN", cmd.name
106+
assert_empty server.commands
107+
end
108+
with_fake_server(
109+
preauth: false, cleartext_login: true, greeting_capabilities: true
110+
) do |server, imap|
111+
imap.config.enforce_logindisabled = :when_capabilities_cached
112+
imap.login("test_user", "test-password")
113+
cmd = server.commands.pop
114+
assert_equal "LOGIN", cmd.name
115+
assert_empty server.commands
116+
end
117+
end
118+
119+
end

0 commit comments

Comments
 (0)