Skip to content

Commit 7b75be9

Browse files
committed
⚡️ Update flag parsing: FLAGS, LIST, PERMANENTFLAGS
Rather than treat all three identically, this uses a more strict parser tailored to each one. One of my earlier tests was invalid, so that has also been fixed. Rather than use `String#scan`, the flags lists are matched with a single regexp, and the match is split then mapped. In my benchmarks, this is _slightly_ faster than using `String#scan`.
1 parent 9d73a4d commit 7b75be9

File tree

5 files changed

+140
-34
lines changed

5 files changed

+140
-34
lines changed

benchmarks/parser.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ benchmark:
116116
response = load_response("../test/net/imap/fixtures/response_parser/fetch_responses.yml",
117117
"test_fetch_msg_att_uid")
118118
script: parser.parse(response)
119+
- name: flags_with_various_flag_types
120+
prelude: |2
121+
response = load_response("../test/net/imap/fixtures/response_parser/flags_responses.yml",
122+
"test_flags_with_various_flag_types")
123+
script: parser.parse(response)
119124
- name: id_gmail
120125
prelude: |2
121126
response = load_response("../test/net/imap/fixtures/response_parser/id_responses.yml",
@@ -191,10 +196,10 @@ benchmark:
191196
response = load_response("../test/net/imap/fixtures/response_parser/thread_responses.yml",
192197
"test_invalid_thread_empty_response")
193198
script: parser.parse(response)
194-
- name: list_with_various_flag_types_and_capitalizations
199+
- name: list_with_various_flag_capitalizations
195200
prelude: |2
196201
response = load_response("../test/net/imap/fixtures/response_parser/list_responses.yml",
197-
"test_list_with_various_flag_types_and_capitalizations")
202+
"test_list_with_various_flag_capitalizations")
198203
script: parser.parse(response)
199204
- name: resp_code_ALREADYEXISTS_rfc9051_7.1_example
200205
prelude: |2
@@ -376,6 +381,11 @@ benchmark:
376381
response = load_response("../test/net/imap/fixtures/response_parser/resp_code_examples.yml",
377382
"test_resp_code_UNSEEN_rfc3501_6.3.1_example")
378383
script: parser.parse(response)
384+
- name: resp_text_PERMANENTFLAGS_with_various_flag_types
385+
prelude: |2
386+
response = load_response("../test/net/imap/fixtures/response_parser/resp_code_examples.yml",
387+
"test_resp_text_PERMANENTFLAGS_with_various_flag_types")
388+
script: parser.parse(response)
379389
- name: resp_text_with_T_LBRA
380390
prelude: |2
381391
response = load_response("../test/net/imap/fixtures/response_parser/resp_text_responses.yml",

lib/net/imap/response_parser.rb

Lines changed: 96 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,54 @@ module RFC3629
185185
CODE_TEXT_CHAR = TEXT_CHAR - RESP_SPECIALS
186186
CODE_TEXT = /#{CODE_TEXT_CHAR}+/n
187187

188+
# flag = "\Answered" / "\Flagged" / "\Deleted" /
189+
# "\Seen" / "\Draft" / flag-keyword / flag-extension
190+
# ; Does not include "\Recent"
191+
# flag-extension = "\" atom
192+
# ; Future expansion. Client implementations
193+
# ; MUST accept flag-extension flags. Server
194+
# ; implementations MUST NOT generate
195+
# ; flag-extension flags except as defined by
196+
# ; a future Standard or Standards Track
197+
# ; revisions of this specification.
198+
# flag-keyword = "$MDNSent" / "$Forwarded" / "$Junk" /
199+
# "$NotJunk" / "$Phishing" / atom
200+
# flag-perm = flag / "\*"
201+
#
202+
# Not checking for max one mbx-list-sflag in the parser.
203+
# >>>
204+
# mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
205+
# "\Subscribed" / "\Remote" / flag-extension
206+
# ; Other flags; multiple from this list are
207+
# ; possible per LIST response, but each flag
208+
# ; can only appear once per LIST response
209+
# mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
210+
# "\Unmarked"
211+
# ; Selectability flags; only one per LIST response
212+
# child-mbox-flag = "\HasChildren" / "\HasNoChildren"
213+
# ; attributes for the CHILDREN return option, at most
214+
# ; one possible per LIST response
215+
FLAG = /\\?#{ATOM}/n
216+
FLAG_EXTENSION = /\\#{ATOM}/n
217+
FLAG_KEYWORD = ATOM
218+
FLAG_PERM = Regexp.union(FLAG, "\\*")
219+
MBX_FLAG = FLAG_EXTENSION
220+
221+
# flag-list = "(" [flag *(SP flag)] ")"
222+
#
223+
# part of resp-text-code:
224+
# >>>
225+
# "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")"
226+
#
227+
# parens from mailbox-list are included in the regexp:
228+
# >>>
229+
# mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
230+
# *(SP mbx-list-oflag) /
231+
# mbx-list-oflag *(SP mbx-list-oflag)
232+
FLAG_LIST = /\G\((#{FLAG }(?:#{SP}#{FLAG })*|)\)/ni
233+
FLAG_PERM_LIST = /\G\((#{FLAG_PERM}(?:#{SP}#{FLAG_PERM})*|)\)/ni
234+
MBX_LIST_FLAGS = /\G\((#{MBX_FLAG }(?:#{SP}#{MBX_FLAG })*|)\)/ni
235+
188236
# RFC3501:
189237
# QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
190238
# "\" quoted-specials
@@ -383,6 +431,14 @@ def label(word)
383431
parse_error("unexpected atom %p, expected %p instead", val, word)
384432
end
385433

434+
# Use #label or #label_in to assert specific known labels
435+
# (+tagged-ext-label+ only, not +atom+).
436+
def label_in(*labels)
437+
lbl = tagged_ext_label and labels.include?(lbl) and return lbl
438+
parse_error("unexpected atom %p, expected one of %s instead",
439+
lbl, labels.join(" or "))
440+
end
441+
386442
# expects "OK" or "PREAUTH" and raises InvalidResponseError on failure
387443
def resp_cond_auth__name
388444
lbl = tagged_ext_label and AUTH_CONDS.include? lbl and return lbl
@@ -1084,18 +1140,21 @@ def header_fld_name
10841140
end
10851141
end
10861142

1143+
# mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
1144+
# "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
1145+
# "STATUS" SP mailbox SP "(" [status-att-list] ")" /
1146+
# number SP "EXISTS" / number SP "RECENT"
1147+
10871148
def mailbox_data__flags
1088-
token = match(T_ATOM)
1089-
name = token.value.upcase
1090-
match(T_SPACE)
1091-
return UntaggedResponse.new(name, flag_list, @str)
1149+
name = label("FLAGS")
1150+
SP!
1151+
UntaggedResponse.new(name, flag_list, @str)
10921152
end
10931153

10941154
def mailbox_data__list
1095-
token = match(T_ATOM)
1096-
name = token.value.upcase
1097-
match(T_SPACE)
1098-
return UntaggedResponse.new(name, mailbox_list, @str)
1155+
name = label_in("LIST", "LSUB", "XLIST")
1156+
SP!
1157+
UntaggedResponse.new(name, mailbox_list, @str)
10991158
end
11001159
alias mailbox_data__lsub mailbox_data__list
11011160
alias mailbox_data__xlist mailbox_data__list
@@ -1645,28 +1704,38 @@ def address
16451704
return Address.new(name, route, mailbox, host)
16461705
end
16471706

1648-
FLAG_REGEXP = /\
1649-
(?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
1650-
(?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
1651-
1707+
# flag-list = "(" [flag *(SP flag)] ")"
16521708
def flag_list
1653-
if @str.index(/\(([^)]*)\)/ni, @pos)
1654-
@pos = $~.end(0)
1655-
return $1.scan(FLAG_REGEXP).collect { |flag, atom|
1656-
if atom
1657-
atom
1658-
else
1659-
flag.capitalize.intern
1660-
end
1661-
}
1662-
else
1663-
parse_error("invalid flag list")
1664-
end
1709+
match_re(Patterns::FLAG_LIST, "flag-list")[1]
1710+
.split(nil)
1711+
.map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1712+
end
1713+
1714+
# "(" [flag-perm *(SP flag-perm)] ")"
1715+
def flag_perm__list
1716+
match_re(Patterns::FLAG_PERM_LIST, "PERMANENTFLAGS flag-perm list")[1]
1717+
.split(nil)
1718+
.map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1719+
end
1720+
1721+
# Not checking for max one mbx-list-sflag in the parser.
1722+
# >>>
1723+
# mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
1724+
# *(SP mbx-list-oflag) /
1725+
# mbx-list-oflag *(SP mbx-list-oflag)
1726+
# mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
1727+
# "\Subscribed" / "\Remote" / flag-extension
1728+
# ; Other flags; multiple from this list are
1729+
# ; possible per LIST response, but each flag
1730+
# ; can only appear once per LIST response
1731+
# mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
1732+
# "\Unmarked"
1733+
# ; Selectability flags; only one per LIST response
1734+
def parens__mbx_list_flags
1735+
match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
1736+
.split(nil).map! { _1.capitalize.to_sym }
16651737
end
16661738

1667-
# TODO: not quite correct. flag-perm != flag
1668-
alias flag_perm__list flag_list
1669-
16701739
# See https://www.rfc-editor.org/errata/rfc3501
16711740
#
16721741
# charset = atom / quoted

test/net/imap/fixtures/response_parser/flags_responses.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,14 @@
1414
- :Seen
1515
- :Draft
1616
raw_data: "* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"
17+
18+
test_flags_with_various_flag_types:
19+
:response: "* FLAGS (\\foo bAR $Etc \\baz)\r\n"
20+
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
21+
name: FLAGS
22+
data:
23+
- :Foo
24+
- bAR
25+
- $Etc
26+
- :Baz
27+
raw_data: "* FLAGS (\\foo bAR $Etc \\baz)\r\n"

test/net/imap/fixtures/response_parser/list_responses.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,19 @@
3434
name: "#news.comp.mail.misc"
3535
raw_data: "* LSUB () \".\" #news.comp.mail.misc\r\n"
3636

37-
test_list_with_various_flag_types_and_capitalizations:
38-
:response: "* LIST (\\foo bAR $Etc \\baz) \".\" \"INBOX\"\r\n"
37+
test_list_with_various_flag_capitalizations:
38+
:response: "* LIST (\\foo \\bAR \\Etc \\baz) \".\" \"INBOX\"\r\n"
3939
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
4040
name: LIST
4141
data: !ruby/struct:Net::IMAP::MailboxList
4242
attr:
4343
- :Foo
44-
- bAR
45-
- $Etc
44+
- :Bar
45+
- :Etc
4646
- :Baz
4747
delim: "."
4848
name: INBOX
49-
raw_data: "* LIST (\\foo bAR $Etc \\baz) \".\" \"INBOX\"\r\n"
49+
raw_data: "* LIST (\\foo \\bAR \\Etc \\baz) \".\" \"INBOX\"\r\n"
5050

5151
test_xlist_inbox:
5252
:response: "* XLIST (\\Inbox) \".\" \"INBOX\"\r\n"

test/net/imap/fixtures/response_parser/resp_code_examples.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,22 @@
8686
text: No permanent flags permitted
8787
raw_data: "* OK [PERMANENTFLAGS ()] No permanent flags permitted\r\n"
8888

89+
test_resp_text_PERMANENTFLAGS_with_various_flag_types:
90+
:response: "* ok [PERMANENTFLAGS (\\foo \\* bAR $Etc \\baz)] flags saved\r\n"
91+
:expected: !ruby/struct:Net::IMAP::UntaggedResponse
92+
name: OK
93+
data: !ruby/struct:Net::IMAP::ResponseText
94+
text: flags saved
95+
code: !ruby/struct:Net::IMAP::ResponseCode
96+
name: PERMANENTFLAGS
97+
data:
98+
- :Foo
99+
- :*
100+
- bAR
101+
- $Etc
102+
- :Baz
103+
raw_data: "* ok [PERMANENTFLAGS (\\foo \\* bAR $Etc \\baz)] flags saved\r\n"
104+
89105
test_resp_code_READ-ONLY_rfc3501_6.3.2_example:
90106
:response: "A932 OK [READ-ONLY] EXAMINE completed\r\n"
91107
:expected: !ruby/struct:Net::IMAP::TaggedResponse

0 commit comments

Comments
 (0)