Skip to content

Commit 7e3a373

Browse files
committed
🔀 Merge pull request #98 from arnt/enable
Add support for ENABLE (RFC 5161) Fixes #33.
2 parents dcc36a8 + 6638c64 commit 7e3a373

File tree

5 files changed

+214
-72
lines changed

5 files changed

+214
-72
lines changed

lib/net/imap.rb

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@ module Net
199199
# [IDLE[https://tools.ietf.org/html/rfc2177]],
200200
# [NAMESPACE[https://tools.ietf.org/html/rfc2342]],
201201
# [UNSELECT[https://tools.ietf.org/html/rfc3691]],
202+
# [ENABLE[https://tools.ietf.org/html/rfc5161]],
202203
#--
203-
# TODO: [ENABLE[https://tools.ietf.org/html/rfc5161]],
204204
# TODO: [LIST-EXTENDED[https://tools.ietf.org/html/rfc5258]],
205205
# TODO: [LIST-STATUS[https://tools.ietf.org/html/rfc5819]],
206206
#++
@@ -257,11 +257,9 @@ module Net
257257
# In addition to the universal commands, the following commands are valid in
258258
# the "_authenticated_" state:
259259
#
260-
#--
261-
# - #enable: <em>Not implemented by Net::IMAP, yet.</em>
260+
# - #enable: Enables backwards incompatible server extensions.
262261
#
263262
# <em>Requires the +ENABLE+ capability.</em>
264-
#++
265263
# - #select: Open a mailbox and enter the "_selected_" state.
266264
# - #examine: Open a mailbox read-only, and enter the "_selected_" state.
267265
# - #create: Creates a new mailbox.
@@ -331,12 +329,11 @@ module Net
331329
#
332330
# Although IMAP4rev2[https://tools.ietf.org/html/rfc9051] is <em>not supported
333331
# yet</em>, Net::IMAP supports several extensions that have been folded into
334-
# it: +IDLE+, +MOVE+, +NAMESPACE+, +UIDPLUS+, and +UNSELECT+.
332+
# it: +ENABLE+, +IDLE+, +MOVE+, +NAMESPACE+, +UIDPLUS+, and +UNSELECT+.
335333
#--
336334
# TODO: RFC4466, ABNF extensions (automatic support for other extensions)
337335
# TODO: +ESEARCH+, ExtendedSearchData
338336
# TODO: +SEARCHRES+,
339-
# TODO: +ENABLE+,
340337
# TODO: +SASL-IR+,
341338
# TODO: +LIST-EXTENDED+,
342339
# TODO: +LIST-STATUS+,
@@ -435,6 +432,11 @@ module Net
435432
# TODO...
436433
#++
437434
#
435+
# ==== RFC5161: +ENABLE+
436+
# Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], so it is also
437+
# listed with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands].
438+
# - #enable: Enables backwards incompatible server extensions.
439+
#
438440
#--
439441
# ==== RFC5182 +SEARCHRES+
440442
# TODO...
@@ -1879,6 +1881,32 @@ def uid_thread(algorithm, search_keys, charset)
18791881
return thread_internal("UID THREAD", algorithm, search_keys, charset)
18801882
end
18811883

1884+
# Sends an {ENABLE command [RFC5161 §3.2]}[https://www.rfc-editor.org/rfc/rfc5161#section-3.1]
1885+
# {[IMAP4rev2 §6.3.1]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.3.1]
1886+
# to enable the specified extenstions, which may be either an
1887+
# array or a string. Returns a list of the extensions that were enabled.
1888+
#
1889+
# Some of the extensions that use ENABLE permit the server to send
1890+
# syntax that this class cannot parse. Caution is advised.
1891+
#
1892+
# The +ENABLE+ command is only valid in the _authenticated_ state, before
1893+
# any mailbox is selected.
1894+
#
1895+
# ===== Capabilities
1896+
#
1897+
# The server's capabilities must include +ENABLE+
1898+
# [RFC5161[https://tools.ietf.org/html/rfc5161]] or IMAP4REV2
1899+
# [RFC9051[https://tools.ietf.org/html/rfc9051]].
1900+
#
1901+
# Additionally, the server capabilities must include a capability matching
1902+
# each enabled extension (usually the same name as the enabled extension).
1903+
def enable(extensions)
1904+
synchronize do
1905+
send_command("ENABLE #{[extensions].flatten.join(' ')}")
1906+
return @responses.delete("ENABLED")[-1]
1907+
end
1908+
end
1909+
18821910
# Sends an {IDLE command [RFC2177 §3]}[https://www.rfc-editor.org/rfc/rfc6851#section-3]
18831911
# {[IMAP4rev2 §6.3.13]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.3.13]
18841912
# that waits for notifications of new or expunged messages. Yields

lib/net/imap/response_parser.rb

Lines changed: 81 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# frozen_string_literal: true
22

33
require_relative "errors"
4+
require_relative "response_parser/parser_utils"
45

56
module Net
67
class IMAP < Protocol
78

89
# Parses an \IMAP server response.
910
class ResponseParser
11+
include ParserUtils
12+
1013
# :call-seq: Net::IMAP::ResponseParser.new -> Net::IMAP::ResponseParser
1114
def initialize
1215
@str = nil
@@ -96,6 +99,45 @@ def parse(str)
9699

97100
Token = Struct.new(:symbol, :value)
98101

102+
# atom = 1*ATOM-CHAR
103+
#
104+
# TODO: match atom entirely by regexp (in the "lexer")
105+
def atom; -combine_adjacent(*ATOM_TOKENS) end
106+
107+
# the #accept version of #atom
108+
def atom?; -combine_adjacent(*ATOM_TOKENS) if lookahead?(*ATOM_TOKENS) end
109+
110+
# Returns <tt>atom.upcase</tt>
111+
def case_insensitive__atom; -combine_adjacent(*ATOM_TOKENS).upcase end
112+
113+
# Returns <tt>atom?&.upcase</tt>
114+
def case_insensitive__atom?
115+
-combine_adjacent(*ATOM_TOKENS).upcase if lookahead?(*ATOM_TOKENS)
116+
end
117+
118+
# In addition to explicitly uses of +tagged-ext-label+, use this to match
119+
# keywords when the grammar has not provided any extension syntax.
120+
#
121+
# Do *not* use this for labels where the grammar specifies extensions
122+
# can be +atom+, even if all currently defined labels would match. For
123+
# example response codes in +resp-text-code+.
124+
#
125+
# tagged-ext-label = tagged-label-fchar *tagged-label-char
126+
# ; Is a valid RFC 3501 "atom".
127+
# tagged-label-fchar = ALPHA / "-" / "_" / "."
128+
# tagged-label-char = tagged-label-fchar / DIGIT / ":"
129+
#
130+
# TODO: add to lexer and only match tagged-ext-label
131+
alias tagged_ext_label case_insensitive__atom
132+
alias tagged_ext_label? case_insensitive__atom?
133+
134+
# Use #label or #label_in to assert specific known labels
135+
# (+tagged-ext-label+ only, not +atom+).
136+
def label(word)
137+
(val = tagged_ext_label) == word and return val
138+
parse_error("unexpected atom %p, expected %p instead", val, word)
139+
end
140+
99141
def response
100142
token = lookahead
101143
case token.symbol
@@ -157,9 +199,11 @@ def response_untagged
157199
when /\A(?:STATUS)\z/ni
158200
return status_response
159201
when /\A(?:CAPABILITY)\z/ni
160-
return capability_response
202+
return capability_data__untagged
161203
when /\A(?:NOOP)\z/ni
162204
return ignored_response
205+
when /\A(?:ENABLED)\z/ni
206+
return enable_data
163207
else
164208
return text_response
165209
end
@@ -967,29 +1011,38 @@ def status_response
9671011
return UntaggedResponse.new(name, data, @str)
9681012
end
9691013

970-
def capability_response
971-
token = match(T_ATOM)
972-
name = token.value.upcase
973-
match(T_SPACE)
974-
UntaggedResponse.new(name, capability_data, @str)
1014+
# The presence of "IMAP4rev1" or "IMAP4rev2" is unenforced here.
1015+
# The grammar rule is used by both response-data and resp-text-code.
1016+
# But this method only returns UntaggedResponse (response-data).
1017+
#
1018+
# RFC3501:
1019+
# capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1"
1020+
# *(SP capability)
1021+
# RFC9051:
1022+
# capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev2"
1023+
# *(SP capability)
1024+
def capability_data__untagged
1025+
UntaggedResponse.new label("CAPABILITY"), capability__list, @str
9751026
end
9761027

977-
def capability_data
978-
data = []
979-
while true
980-
token = lookahead
981-
case token.symbol
982-
when T_CRLF, T_RBRA
983-
break
984-
when T_SPACE
985-
shift_token
986-
next
987-
end
988-
data.push(atom.upcase)
989-
end
990-
data
1028+
# enable-data = "ENABLED" *(SP capability)
1029+
def enable_data
1030+
UntaggedResponse.new label("ENABLED"), capability__list, @str
1031+
end
1032+
1033+
# As a workaround for buggy servers, allow a trailing SP:
1034+
# *(SP capapility) [SP]
1035+
def capability__list
1036+
data = []; while _ = SP? && capability? do data << _ end; data
9911037
end
9921038

1039+
# capability = ("AUTH=" auth-type) / atom
1040+
# ; New capabilities MUST begin with "X" or be
1041+
# ; registered with IANA as standard or
1042+
# ; standards-track
1043+
alias capability case_insensitive__atom
1044+
alias capability? case_insensitive__atom?
1045+
9931046
def id_response
9941047
token = match(T_ATOM)
9951048
name = token.value.upcase
@@ -1125,7 +1178,7 @@ def resp_text_code
11251178
when /\A(?:BADCHARSET)\z/n
11261179
result = ResponseCode.new(name, charset_list)
11271180
when /\A(?:CAPABILITY)\z/ni
1128-
result = ResponseCode.new(name, capability_data)
1181+
result = ResponseCode.new(name, capability__list)
11291182
when /\A(?:PERMANENTFLAGS)\z/n
11301183
match(T_SPACE)
11311184
result = ResponseCode.new(name, flag_list)
@@ -1321,10 +1374,6 @@ def case_insensitive_string
13211374
T_PLUS
13221375
]
13231376

1324-
def atom
1325-
-combine_adjacent(*ATOM_TOKENS)
1326-
end
1327-
13281377
# ASTRING-CHAR = ATOM-CHAR / resp-specials
13291378
# resp-specials = "]"
13301379
ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA]
@@ -1396,12 +1445,18 @@ def nil_atom
13961445
# This advances @pos directly so it's safe before changing @lex_state.
13971446
def accept_space
13981447
if @token
1399-
shift_token if @token.symbol == T_SPACE
1448+
if @token.symbol == T_SPACE
1449+
shift_token
1450+
" "
1451+
end
14001452
elsif @str[@pos] == " "
14011453
@pos += 1
1454+
" "
14021455
end
14031456
end
14041457

1458+
alias SP? accept_space
1459+
14051460
# The RFC is very strict about this and usually we should be too.
14061461
# But skipping spaces is usually a safe workaround for buggy servers.
14071462
#
@@ -1413,46 +1468,6 @@ def accept_spaces
14131468
end
14141469
end
14151470

1416-
def match(*args, lex_state: @lex_state)
1417-
if @token && lex_state != @lex_state
1418-
parse_error("invalid lex_state change to %s with unconsumed token",
1419-
lex_state)
1420-
end
1421-
begin
1422-
@lex_state, original_lex_state = lex_state, @lex_state
1423-
token = lookahead
1424-
unless args.include?(token.symbol)
1425-
parse_error('unexpected token %s (expected %s)',
1426-
token.symbol.id2name,
1427-
args.collect {|i| i.id2name}.join(" or "))
1428-
end
1429-
shift_token
1430-
return token
1431-
ensure
1432-
@lex_state = original_lex_state
1433-
end
1434-
end
1435-
1436-
# like match, but does not raise error on failure.
1437-
#
1438-
# returns and shifts token on successful match
1439-
# returns nil and leaves @token unshifted on no match
1440-
def accept(*args)
1441-
token = lookahead
1442-
if args.include?(token.symbol)
1443-
shift_token
1444-
token
1445-
end
1446-
end
1447-
1448-
def lookahead
1449-
@token ||= next_token
1450-
end
1451-
1452-
def shift_token
1453-
@token = nil
1454-
end
1455-
14561471
def next_token
14571472
case @lex_state
14581473
when EXPR_BEG
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
module Net
4+
class IMAP < Protocol
5+
class ResponseParser
6+
# basic utility methods for parsing.
7+
#
8+
# (internal API, subject to change)
9+
module ParserUtils # :nodoc:
10+
11+
private
12+
13+
def match(*args, lex_state: @lex_state)
14+
if @token && lex_state != @lex_state
15+
parse_error("invalid lex_state change to %s with unconsumed token",
16+
lex_state)
17+
end
18+
begin
19+
@lex_state, original_lex_state = lex_state, @lex_state
20+
token = lookahead
21+
unless args.include?(token.symbol)
22+
parse_error('unexpected token %s (expected %s)',
23+
token.symbol.id2name,
24+
args.collect {|i| i.id2name}.join(" or "))
25+
end
26+
shift_token
27+
return token
28+
ensure
29+
@lex_state = original_lex_state
30+
end
31+
end
32+
33+
# like match, but does not raise error on failure.
34+
#
35+
# returns and shifts token on successful match
36+
# returns nil and leaves @token unshifted on no match
37+
def accept(*args)
38+
token = lookahead
39+
if args.include?(token.symbol)
40+
shift_token
41+
token
42+
end
43+
end
44+
45+
# like accept, without consuming the token
46+
def lookahead?(*symbols)
47+
@token if symbols.include?((@token ||= next_token)&.symbol)
48+
end
49+
50+
def lookahead
51+
@token ||= next_token
52+
end
53+
54+
def shift_token
55+
@token = nil
56+
end
57+
end
58+
end
59+
end
60+
end

0 commit comments

Comments
 (0)