Skip to content

Commit 7f61f71

Browse files
committed
✨ Support LITERAL+ and LITERAL- extensions
This also adds a new config attribute: `max_non_synchronizing_literal`. By default, it is set rather conservatively to 16 KiB. But servers are much more likely to support `LITERAL-` than `LITERAL+`, so the practical limit will be 4096.
1 parent f4b5bf2 commit 7f61f71

4 files changed

Lines changed: 80 additions & 7 deletions

File tree

lib/net/imap.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,16 +450,16 @@ module Net
450450
#
451451
# Although IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] is not supported
452452
# yet, Net::IMAP supports several extensions that have been folded into it:
453-
# +ENABLE+, +IDLE+, +MOVE+, +NAMESPACE+, +SASL-IR+, +UIDPLUS+, +UNSELECT+,
454-
# <tt>STATUS=SIZE</tt>, and the fetch side of +BINARY+.
453+
# +ENABLE+, +IDLE+, +LITERAL-+, +MOVE+, +NAMESPACE+, +SASL-IR+, +UIDPLUS+,
454+
# +UNSELECT+, <tt>STATUS=SIZE</tt>, and the fetch side of +BINARY+.
455455
# Commands for these extensions are listed with the {Core IMAP
456456
# commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands], above.
457457
#
458458
# >>>
459459
# <em>The following are folded into +IMAP4rev2+ but are currently
460460
# unsupported or incompletely supported by</em> Net::IMAP<em>: RFC4466
461461
# extensions, +SEARCHRES+, +LIST-EXTENDED+, +LIST-STATUS+,
462-
# +LITERAL-+, and +SPECIAL-USE+.</em>
462+
# and +SPECIAL-USE+.</em>
463463
#
464464
# ==== RFC2087: +QUOTA+
465465
# +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
@@ -569,6 +569,15 @@ module Net
569569
# - Updates #store and #uid_store with the +unchangedsince+ modifier and adds
570570
# the +MODIFIED+ ResponseCode to the tagged response.
571571
#
572+
# ==== RFC7888: <tt>LITERAL+</tt>
573+
# - Literal strings smaller than Config#max_non_synchronizing_literal bytes
574+
# are sent without waiting for the server's continuation request.
575+
#
576+
# ==== RFC7888: +LITERAL-+
577+
# - Literal strings smaller than 4096 bytes or
578+
# Config#max_non_synchronizing_literal (whichever is smaller)
579+
# are sent without waiting for the server's continuation request.
580+
#
572581
# ==== RFC8438: <tt>STATUS=SIZE</tt>
573582
# - Updates #status with the +SIZE+ status attribute.
574583
#

lib/net/imap/command_data.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ def send_binary_literal(*, **) = send_literal(*, **, binary: true)
8686
# TODO: raise or warn when capabilities don't allow non_sync.
8787
# * `false` -> Force normal synchronizing literal behavior.
8888
# * `nil` -> (default) Currently behaves like `false` (will be dynamic).
89-
# TODO: Dynamic, based on capabilities and bytesize.
9089
def send_literal(str, tag = nil, binary: false, non_sync: nil)
9190
synchronize do
91+
non_sync = non_sync_literal?(str.bytesize) if non_sync.nil?
9292
prefix = "~" if binary
9393
plus = "+" if non_sync
9494
put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
@@ -110,6 +110,13 @@ def send_literal(str, tag = nil, binary: false, non_sync: nil)
110110
end
111111
end
112112

113+
def non_sync_literal?(bytesize)
114+
capabilities_cached? &&
115+
bytesize <= config.max_non_synchronizing_literal &&
116+
(capable?("LITERAL+") ||
117+
bytesize <= 4096 && (capable?("IMAP4rev2") || capable?("LITERAL-")))
118+
end
119+
113120
def send_number_data(num)
114121
put_string(num.to_s)
115122
end

lib/net/imap/config.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,40 @@ def self.[](config)
281281
0.5r => true,
282282
}
283283

284+
# The maximum bytesize for sending non-synchronizing literals, when the
285+
# server supports them. To disable non-synchronizing literals, set the
286+
# value to +-1+.
287+
#
288+
# Non-synchronizing literals are only sent when the server's
289+
# capabilities[rdoc-ref:IMAP#capabilities] have been
290+
# cached[rdoc-ref:IMAP#capabilities_cached?] and include either
291+
# <tt>LITERAL+</tt> [RFC7888[https://www.rfc-editor.org/rfc/rfc7888]],
292+
# <tt>LITERAL-</tt> [RFC7888[https://www.rfc-editor.org/rfc/rfc7888]], or
293+
# +IMAP4rev2+ [RFC9051[https://www.rfc-editor.org/rfc/rfc9051]].
294+
#
295+
# For <tt>LITERAL+</tt>, this value is the only limit on whether a literal
296+
# value is sent as non-synchronizing literals. For <tt>LITERAL-</tt> and
297+
# <tt>IMAP4rev2</tt>, non-synchronizing literals must also be smaller than
298+
# +4096+ bytes.
299+
#
300+
# Non-synchronizing literals avoid the latency of waiting for the server
301+
# to allow continuation. However, if a client sends a non-synchronizing
302+
# literal that is too large for the server, the server may need to close
303+
# the connection. Because <tt>LITERAL+</tt> does not directly indicate
304+
# the server's limits, it's best to avoid sending very large
305+
# non-synchronized literals.
306+
#
307+
# ==== Versioned Defaults
308+
#
309+
# max_non_synchronizing_literal <em>was added in +v0.6.4+.</em>
310+
#
311+
# * original: +-1+ (_never_ send non-synchronizing literals)
312+
# * +0.6+: 16 KiB
313+
attr_accessor :max_non_synchronizing_literal, type: Integer?, defaults: {
314+
0.0r => -1,
315+
0.6r => 16 << 16, # 16 KiB
316+
}
317+
284318
# The maximum allowed server response size. When +nil+, there is no limit
285319
# on response size.
286320
#

test/net/imap/test_imap.rb

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -673,20 +673,38 @@ def test_send_invalid_number
673673
end
674674

675675
test("send literal args") do
676-
with_fake_server do |server, imap|
676+
with_fake_server(with_extensions: %w[LITERAL-]) do |server, imap|
677+
# disable automatic non-synchronizing literals
678+
imap.config.max_non_synchronizing_literal = -1
677679
server.on "TEST", &:done_ok
678680
send_args = ->(*args) do
679681
imap.__send__(:send_command, "TEST", *args)
680682
end
681683
send_args.call ["\xDE\xAD\xBE\xEF".b]
682684
assert_equal "({4}\r\n\xDE\xAD\xBE\xEF)".b, server.commands.pop.args
683685

686+
# enable automatic non-synchronizing literals
687+
imap.config.max_non_synchronizing_literal = 1024
684688
buff = bytes = nil
685689
server.literal_acceptor = proc { buff, bytes = _1, _2; false }
690+
server.on "TEST", &:done_ok
691+
send_args = ->(*args) do
692+
imap.__send__(:send_command, "TEST", *args)
693+
end
694+
send_args.call ["\xDE\xAD\xBE\xEF".b]
695+
assert_equal "({4+}\r\n\xDE\xAD\xBE\xEF)".b, server.commands.pop.args
696+
assert_nil buff
697+
assert_nil bytes
698+
699+
# limited automatic non-synchronizing literals
700+
imap.config.max_non_synchronizing_literal = 5
686701
assert_raise(Net::IMAP::NoResponseError) do
687-
send_args.call Net::IMAP::Literal["\x01" * 10]
702+
send_args.call [
703+
Net::IMAP::Literal["\rhi\r"],
704+
Net::IMAP::Literal["\x01" * 10],
705+
]
688706
end
689-
assert_match(/TEST \{10\}\r\n\z/, buff)
707+
assert_match(/TEST \(\{4\+\}\r\n\rhi\r \{10\}\r\n\z/, buff)
690708
assert_equal 10, bytes
691709
assert_empty server.commands
692710

@@ -703,18 +721,23 @@ def test_send_invalid_number
703721
assert_nil buff
704722
assert_nil bytes
705723

724+
imap.config.max_non_synchronizing_literal = 5
706725
server.literal_acceptor = proc { true }
707726
send_args.call("literal", Net::IMAP::Literal["\r", false],
727+
"literal", Net::IMAP::Literal["αβ", nil],
708728
"literal", Net::IMAP::Literal["αβγδε", nil],
709729
"literal+", Net::IMAP::Literal["αβγδε", true],
710730
"literal8", Net::IMAP::Literal8["\0", false],
731+
"literal8+", Net::IMAP::Literal8["\0" * 2, nil],
711732
"literal8", Net::IMAP::Literal8["\0" * 6, nil],
712733
"literal8+", Net::IMAP::Literal8["\0" * 8, true],
713734
"done")
714735
assert_equal("literal" " {1}\r\n\r " \
736+
"literal" " {4+}\r\nαβ " \
715737
"literal" " {10}\r\nαβγδε " \
716738
"literal+" " {10+}\r\nαβγδε " \
717739
"literal8" " ~{1}\r\n\0 " \
740+
"literal8+" " ~{2+}\r\n\0\0 " \
718741
"literal8" " ~{6}\r\n\0\0\0\0\0\0 " \
719742
"literal8+" " ~{8+}\r\n\0\0\0\0\0\0\0\0 " \
720743
"done".b,

0 commit comments

Comments
 (0)