Skip to content

Commit ad3e0fa

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, wo offer the `enforce_logindisabled` config option. Fixes #32.
1 parent d276458 commit ad3e0fa

File tree

4 files changed

+156
-7
lines changed

4 files changed

+156
-7
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_login.rb

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 raises LoginDisabledError when LOGINDISABLED" do
26+
with_fake_server(preauth: false, cleartext_login: false) do |server, imap|
27+
assert imap.capabilities_cached?
28+
assert_raise(Net::IMAP::LoginDisabledError) do
29+
imap.login("test_user", "test-password")
30+
end
31+
assert_empty server.commands
32+
end
33+
end
34+
35+
test "#login first checks capabilities for LOGINDISABLED (success)" do
36+
with_fake_server(
37+
preauth: false, cleartext_login: true, greeting_capabilities: false
38+
) do |server, imap|
39+
imap.login("test_user", "test-password")
40+
cmd = server.commands.pop
41+
assert_equal "CAPABILITY", cmd.name
42+
cmd = server.commands.pop
43+
assert_equal "LOGIN", cmd.name
44+
assert_empty server.commands
45+
end
46+
end
47+
48+
test "#login first checks capabilities for LOGINDISABLED (failure)" do
49+
with_fake_server(
50+
preauth: false, cleartext_login: false, greeting_capabilities: false
51+
) do |server, imap|
52+
assert_raise(Net::IMAP::LoginDisabledError) do
53+
imap.login("test_user", "test-password")
54+
end
55+
cmd = server.commands.pop
56+
assert_equal "CAPABILITY", cmd.name
57+
assert_empty server.commands
58+
end
59+
end
60+
61+
test("#login sends LOGIN without asking CAPABILITY " \
62+
"when config.enforce_logindisabled is false") do
63+
with_fake_server(
64+
preauth: false, cleartext_login: false, greeting_capabilities: false
65+
) do |server, imap|
66+
imap.config.enforce_logindisabled = false
67+
imap.login("test_user", "test-password")
68+
cmd = server.commands.pop
69+
assert_equal "LOGIN", cmd.name
70+
end
71+
end
72+
73+
test("#login raises LoginDisabledError without sending CAPABILITY " \
74+
"when config.enforce_logindisabled is :when_capabilities_cached") do
75+
with_fake_server(
76+
preauth: false, cleartext_login: false, greeting_capabilities: true
77+
) do |server, imap|
78+
imap.config.enforce_logindisabled = :when_capabilities_cached
79+
assert_raise(Net::IMAP::LoginDisabledError) do
80+
imap.login("test_user", "test-password")
81+
end
82+
assert_empty server.commands
83+
end
84+
end
85+
86+
test("#login sends LOGIN without asking CAPABILITY " \
87+
"when config.enforce_logindisabled is :when_capabilities_cached") do
88+
with_fake_server(
89+
preauth: false, cleartext_login: false, greeting_capabilities: false
90+
) do |server, imap|
91+
imap.config.enforce_logindisabled = :when_capabilities_cached
92+
imap.login("test_user", "test-password")
93+
cmd = server.commands.pop
94+
assert_equal "LOGIN", cmd.name
95+
assert_empty server.commands
96+
end
97+
with_fake_server(
98+
preauth: false, cleartext_login: true, greeting_capabilities: true
99+
) do |server, imap|
100+
imap.config.enforce_logindisabled = :when_capabilities_cached
101+
imap.login("test_user", "test-password")
102+
cmd = server.commands.pop
103+
assert_equal "LOGIN", cmd.name
104+
assert_empty server.commands
105+
end
106+
end
107+
108+
end

0 commit comments

Comments
 (0)