net-imap 0.3.7 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of net-imap might be problematic. Click here for more details.

Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +46 -0
  3. data/.github/workflows/test.yml +5 -12
  4. data/.gitignore +2 -0
  5. data/Gemfile +3 -0
  6. data/README.md +15 -4
  7. data/Rakefile +0 -7
  8. data/docs/styles.css +0 -12
  9. data/lib/net/imap/authenticators.rb +26 -57
  10. data/lib/net/imap/command_data.rb +13 -6
  11. data/lib/net/imap/data_encoding.rb +14 -2
  12. data/lib/net/imap/deprecated_client_options.rb +139 -0
  13. data/lib/net/imap/errors.rb +20 -0
  14. data/lib/net/imap/fetch_data.rb +518 -0
  15. data/lib/net/imap/response_data.rb +116 -252
  16. data/lib/net/imap/response_parser/parser_utils.rb +240 -0
  17. data/lib/net/imap/response_parser.rb +1535 -1003
  18. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  19. data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
  20. data/lib/net/imap/sasl/authenticators.rb +118 -0
  21. data/lib/net/imap/sasl/client_adapter.rb +72 -0
  22. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +21 -11
  23. data/lib/net/imap/sasl/digest_md5_authenticator.rb +180 -0
  24. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  25. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  26. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +25 -16
  27. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  28. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  29. data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
  30. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  31. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  32. data/lib/net/imap/sasl/stringprep.rb +6 -66
  33. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  34. data/lib/net/imap/sasl.rb +144 -43
  35. data/lib/net/imap/sasl_adapter.rb +21 -0
  36. data/lib/net/imap/sequence_set.rb +67 -0
  37. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  38. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  39. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  40. data/lib/net/imap/stringprep/tables.rb +146 -0
  41. data/lib/net/imap/stringprep/trace.rb +85 -0
  42. data/lib/net/imap/stringprep.rb +159 -0
  43. data/lib/net/imap.rb +1055 -612
  44. data/net-imap.gemspec +4 -3
  45. data/rakelib/benchmarks.rake +91 -0
  46. data/rakelib/saslprep.rake +4 -4
  47. data/rakelib/string_prep_tables_generator.rb +82 -60
  48. metadata +31 -13
  49. data/benchmarks/stringprep.yml +0 -65
  50. data/benchmarks/table-regexps.yml +0 -39
  51. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  52. data/lib/net/imap/authenticators/plain.rb +0 -41
  53. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  54. data/lib/net/imap/sasl/saslprep.rb +0 -55
  55. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  56. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "errors"
4
+ require_relative "response_parser/parser_utils"
4
5
 
5
6
  module Net
6
7
  class IMAP < Protocol
7
8
 
8
9
  # Parses an \IMAP server response.
9
10
  class ResponseParser
11
+ include ParserUtils
12
+ extend ParserUtils::Generator
13
+
10
14
  # :call-seq: Net::IMAP::ResponseParser.new -> Net::IMAP::ResponseParser
11
15
  def initialize
12
16
  @str = nil
@@ -33,218 +37,897 @@ module Net
33
37
 
34
38
  # :stopdoc:
35
39
 
36
- EXPR_BEG = :EXPR_BEG
37
- EXPR_DATA = :EXPR_DATA
38
- EXPR_TEXT = :EXPR_TEXT
39
- EXPR_RTEXT = :EXPR_RTEXT
40
- EXPR_CTEXT = :EXPR_CTEXT
41
-
42
- T_SPACE = :SPACE
43
- T_NIL = :NIL
44
- T_NUMBER = :NUMBER
45
- T_ATOM = :ATOM
46
- T_QUOTED = :QUOTED
47
- T_LPAR = :LPAR
48
- T_RPAR = :RPAR
49
- T_BSLASH = :BSLASH
50
- T_STAR = :STAR
51
- T_LBRA = :LBRA
52
- T_RBRA = :RBRA
53
- T_LITERAL = :LITERAL
54
- T_PLUS = :PLUS
55
- T_PERCENT = :PERCENT
56
- T_CRLF = :CRLF
57
- T_EOF = :EOF
58
- T_TEXT = :TEXT
59
-
40
+ EXPR_BEG = :EXPR_BEG # the default, used in most places
41
+ EXPR_DATA = :EXPR_DATA # envelope, body(structure), namespaces
42
+
43
+ T_SPACE = :SPACE # atom special
44
+ T_ATOM = :ATOM # atom (subset of astring chars)
45
+ T_NIL = :NIL # subset of atom and label
46
+ T_NUMBER = :NUMBER # subset of atom
47
+ T_LBRA = :LBRA # subset of atom
48
+ T_PLUS = :PLUS # subset of atom; tag special
49
+ T_RBRA = :RBRA # atom special; resp_special; valid astring char
50
+ T_QUOTED = :QUOTED # starts/end with atom special
51
+ T_BSLASH = :BSLASH # atom special; quoted special
52
+ T_LPAR = :LPAR # atom special; paren list delimiter
53
+ T_RPAR = :RPAR # atom special; paren list delimiter
54
+ T_STAR = :STAR # atom special; list wildcard
55
+ T_PERCENT = :PERCENT # atom special; list wildcard
56
+ T_LITERAL = :LITERAL # starts with atom special
57
+ T_LITERAL8 = :LITERAL8 # starts with atom char "~"
58
+ T_CRLF = :CRLF # atom special; text special; quoted special
59
+ T_TEXT = :TEXT # any char except CRLF
60
+ T_EOF = :EOF # end of response string
61
+
62
+ module ResponseConditions
63
+ OK = "OK"
64
+ NO = "NO"
65
+ BAD = "BAD"
66
+ BYE = "BYE"
67
+ PREAUTH = "PREAUTH"
68
+
69
+ RESP_COND_STATES = [OK, NO, BAD ].freeze
70
+ RESP_DATA_CONDS = [OK, NO, BAD, BYE, ].freeze
71
+ AUTH_CONDS = [OK, PREAUTH].freeze
72
+ GREETING_CONDS = [OK, BYE, PREAUTH].freeze
73
+ RESP_CONDS = [OK, NO, BAD, BYE, PREAUTH].freeze
74
+ end
75
+ include ResponseConditions
76
+
77
+ module Patterns
78
+
79
+ module CharClassSubtraction
80
+ refine Regexp do
81
+ def -(rhs); /[#{source}&&[^#{rhs.source}]]/n.freeze end
82
+ end
83
+ end
84
+ using CharClassSubtraction
85
+
86
+ # From RFC5234, "Augmented BNF for Syntax Specifications: ABNF"
87
+ # >>>
88
+ # ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
89
+ # CHAR = %x01-7F
90
+ # CRLF = CR LF
91
+ # ; Internet standard newline
92
+ # CTL = %x00-1F / %x7F
93
+ # ; controls
94
+ # DIGIT = %x30-39
95
+ # ; 0-9
96
+ # DQUOTE = %x22
97
+ # ; " (Double Quote)
98
+ # HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
99
+ # OCTET = %x00-FF
100
+ # SP = %x20
101
+ module RFC5234
102
+ ALPHA = /[A-Za-z]/n
103
+ CHAR = /[\x01-\x7f]/n
104
+ CRLF = /\r\n/n
105
+ CTL = /[\x00-\x1F\x7F]/n
106
+ DIGIT = /\d/n
107
+ DQUOTE = /"/n
108
+ HEXDIG = /\h/
109
+ OCTET = /[\x00-\xFF]/n # not using /./m for embedding purposes
110
+ SP = / /n
111
+ end
112
+
113
+ # UTF-8, a transformation format of ISO 10646
114
+ # >>>
115
+ # UTF8-1 = %x00-7F
116
+ # UTF8-tail = %x80-BF
117
+ # UTF8-2 = %xC2-DF UTF8-tail
118
+ # UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
119
+ # %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
120
+ # UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
121
+ # %xF4 %x80-8F 2( UTF8-tail )
122
+ # UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
123
+ # UTF8-octets = *( UTF8-char )
124
+ #
125
+ # n.b. String * Integer is used for repetition, rather than /x{3}/,
126
+ # because ruby 3.2's linear-time cache-based optimization doesn't work
127
+ # with "bounded or fixed times repetition nesting in another repetition
128
+ # (e.g. /(a{2,3})*/). It is an implementation issue entirely, but we
129
+ # believe it is hard to support this case correctly."
130
+ # See https://bugs.ruby-lang.org/issues/19104
131
+ module RFC3629
132
+ UTF8_1 = /[\x00-\x7f]/n # aka ASCII 7bit
133
+ UTF8_TAIL = /[\x80-\xBF]/n
134
+ UTF8_2 = /[\xC2-\xDF]#{UTF8_TAIL}/n
135
+ UTF8_3 = Regexp.union(/\xE0[\xA0-\xBF]#{UTF8_TAIL}/n,
136
+ /\xED[\x80-\x9F]#{UTF8_TAIL}/n,
137
+ /[\xE1-\xEC]#{ UTF8_TAIL.source * 2}/n,
138
+ /[\xEE-\xEF]#{ UTF8_TAIL.source * 2}/n)
139
+ UTF8_4 = Regexp.union(/[\xF1-\xF3]#{ UTF8_TAIL.source * 3}/n,
140
+ /\xF0[\x90-\xBF]#{UTF8_TAIL.source * 2}/n,
141
+ /\xF4[\x80-\x8F]#{UTF8_TAIL.source * 2}/n)
142
+ UTF8_CHAR = Regexp.union(UTF8_1, UTF8_2, UTF8_3, UTF8_4)
143
+ UTF8_OCTETS = /#{UTF8_CHAR}*/n
144
+ end
145
+
146
+ include RFC5234
147
+ include RFC3629
148
+
149
+ # CHAR8 = %x01-ff
150
+ # ; any OCTET except NUL, %x00
151
+ CHAR8 = /[\x01-\xff]/n
152
+
153
+ # list-wildcards = "%" / "*"
154
+ LIST_WILDCARDS = /[%*]/n
155
+ # quoted-specials = DQUOTE / "\"
156
+ QUOTED_SPECIALS = /["\\]/n
157
+ # resp-specials = "]"
158
+ RESP_SPECIALS = /[\]]/n
159
+
160
+ # atomish = 1*<any ATOM-CHAR except "[">
161
+ # ; We use "atomish" for msg-att and section, in order
162
+ # ; to simplify "BODY[HEADER.FIELDS (foo bar)]".
163
+ #
164
+ # atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards /
165
+ # quoted-specials / resp-specials
166
+ # ATOM-CHAR = <any CHAR except atom-specials>
167
+ # atom = 1*ATOM-CHAR
168
+ # ASTRING-CHAR = ATOM-CHAR / resp-specials
169
+ # tag = 1*<any ASTRING-CHAR except "+">
170
+
171
+ ATOM_SPECIALS = /[(){ \x00-\x1f\x7f%*"\\\]]/n
172
+ ASTRING_SPECIALS = /[(){ \x00-\x1f\x7f%*"\\]/n
173
+
174
+ ASTRING_CHAR = CHAR - ASTRING_SPECIALS
175
+ ATOM_CHAR = CHAR - ATOM_SPECIALS
176
+
177
+ ATOM = /#{ATOM_CHAR}+/n
178
+ ASTRING_CHARS = /#{ASTRING_CHAR}+/n
179
+ ATOMISH = /#{ATOM_CHAR - /[\[]/ }+/
180
+ TAG = /#{ASTRING_CHAR - /[+]/ }+/
181
+
182
+ # TEXT-CHAR = <any CHAR except CR and LF>
183
+ TEXT_CHAR = CHAR - /[\r\n]/
184
+
185
+ # resp-text-code = ... / atom [SP 1*<any TEXT-CHAR except "]">]
186
+ CODE_TEXT_CHAR = TEXT_CHAR - RESP_SPECIALS
187
+ CODE_TEXT = /#{CODE_TEXT_CHAR}+/n
188
+
189
+ # flag = "\Answered" / "\Flagged" / "\Deleted" /
190
+ # "\Seen" / "\Draft" / flag-keyword / flag-extension
191
+ # ; Does not include "\Recent"
192
+ # flag-extension = "\" atom
193
+ # ; Future expansion. Client implementations
194
+ # ; MUST accept flag-extension flags. Server
195
+ # ; implementations MUST NOT generate
196
+ # ; flag-extension flags except as defined by
197
+ # ; a future Standard or Standards Track
198
+ # ; revisions of this specification.
199
+ # flag-keyword = "$MDNSent" / "$Forwarded" / "$Junk" /
200
+ # "$NotJunk" / "$Phishing" / atom
201
+ # flag-perm = flag / "\*"
202
+ #
203
+ # Not checking for max one mbx-list-sflag in the parser.
204
+ # >>>
205
+ # mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
206
+ # "\Subscribed" / "\Remote" / flag-extension
207
+ # ; Other flags; multiple from this list are
208
+ # ; possible per LIST response, but each flag
209
+ # ; can only appear once per LIST response
210
+ # mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
211
+ # "\Unmarked"
212
+ # ; Selectability flags; only one per LIST response
213
+ # child-mbox-flag = "\HasChildren" / "\HasNoChildren"
214
+ # ; attributes for the CHILDREN return option, at most
215
+ # ; one possible per LIST response
216
+ FLAG = /\\?#{ATOM}/n
217
+ FLAG_EXTENSION = /\\#{ATOM}/n
218
+ FLAG_KEYWORD = ATOM
219
+ FLAG_PERM = Regexp.union(FLAG, "\\*")
220
+ MBX_FLAG = FLAG_EXTENSION
221
+
222
+ # flag-list = "(" [flag *(SP flag)] ")"
223
+ #
224
+ # part of resp-text-code:
225
+ # >>>
226
+ # "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")"
227
+ #
228
+ # parens from mailbox-list are included in the regexp:
229
+ # >>>
230
+ # mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
231
+ # *(SP mbx-list-oflag) /
232
+ # mbx-list-oflag *(SP mbx-list-oflag)
233
+ FLAG_LIST = /\G\((#{FLAG }(?:#{SP}#{FLAG })*|)\)/ni
234
+ FLAG_PERM_LIST = /\G\((#{FLAG_PERM}(?:#{SP}#{FLAG_PERM})*|)\)/ni
235
+ MBX_LIST_FLAGS = /\G\((#{MBX_FLAG }(?:#{SP}#{MBX_FLAG })*|)\)/ni
236
+
237
+ # RFC3501:
238
+ # QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
239
+ # "\" quoted-specials
240
+ # RFC9051:
241
+ # QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
242
+ # "\" quoted-specials / UTF8-2 / UTF8-3 / UTF8-4
243
+ # RFC3501 & RFC9051:
244
+ # quoted = DQUOTE *QUOTED-CHAR DQUOTE
245
+ QUOTED_CHAR_safe = TEXT_CHAR - QUOTED_SPECIALS
246
+ QUOTED_CHAR_esc = /\\#{QUOTED_SPECIALS}/n
247
+ QUOTED_CHAR_rev1 = Regexp.union(QUOTED_CHAR_safe, QUOTED_CHAR_esc)
248
+ QUOTED_CHAR_rev2 = Regexp.union(QUOTED_CHAR_rev1,
249
+ UTF8_2, UTF8_3, UTF8_4)
250
+ QUOTED_rev1 = /"(#{QUOTED_CHAR_rev1}*)"/n
251
+ QUOTED_rev2 = /"(#{QUOTED_CHAR_rev2}*)"/n
252
+
253
+ # RFC3501:
254
+ # text = 1*TEXT-CHAR
255
+ # RFC9051:
256
+ # text = 1*(TEXT-CHAR / UTF8-2 / UTF8-3 / UTF8-4)
257
+ # ; Non-ASCII text can only be returned
258
+ # ; after ENABLE IMAP4rev2 command
259
+ TEXT_rev1 = /#{TEXT_CHAR}+/
260
+ TEXT_rev2 = /#{Regexp.union TEXT_CHAR, UTF8_2, UTF8_3, UTF8_4}+/
261
+
262
+ # tagged-label-fchar = ALPHA / "-" / "_" / "."
263
+ TAGGED_LABEL_FCHAR = /[a-zA-Z\-_.]/n
264
+ # tagged-label-char = tagged-label-fchar / DIGIT / ":"
265
+ TAGGED_LABEL_CHAR = /[a-zA-Z\-_.0-9:]*/n
266
+ # tagged-ext-label = tagged-label-fchar *tagged-label-char
267
+ # ; Is a valid RFC 3501 "atom".
268
+ TAGGED_EXT_LABEL = /#{TAGGED_LABEL_FCHAR}#{TAGGED_LABEL_CHAR}*/n
269
+
270
+ # nz-number = digit-nz *DIGIT
271
+ # ; Non-zero unsigned 32-bit integer
272
+ # ; (0 < n < 4,294,967,296)
273
+ NZ_NUMBER = /[1-9]\d*/n
274
+
275
+ # seq-number = nz-number / "*"
276
+ # ; message sequence number (COPY, FETCH, STORE
277
+ # ; commands) or unique identifier (UID COPY,
278
+ # ; UID FETCH, UID STORE commands).
279
+ # ; * represents the largest number in use. In
280
+ # ; the case of message sequence numbers, it is
281
+ # ; the number of messages in a non-empty mailbox.
282
+ # ; In the case of unique identifiers, it is the
283
+ # ; unique identifier of the last message in the
284
+ # ; mailbox or, if the mailbox is empty, the
285
+ # ; mailbox's current UIDNEXT value.
286
+ # ; The server should respond with a tagged BAD
287
+ # ; response to a command that uses a message
288
+ # ; sequence number greater than the number of
289
+ # ; messages in the selected mailbox. This
290
+ # ; includes "*" if the selected mailbox is empty.
291
+ SEQ_NUMBER = /#{NZ_NUMBER}|\*/n
292
+
293
+ # seq-range = seq-number ":" seq-number
294
+ # ; two seq-number values and all values between
295
+ # ; these two regardless of order.
296
+ # ; Example: 2:4 and 4:2 are equivalent and
297
+ # ; indicate values 2, 3, and 4.
298
+ # ; Example: a unique identifier sequence range of
299
+ # ; 3291:* includes the UID of the last message in
300
+ # ; the mailbox, even if that value is less than
301
+ # ; 3291.
302
+ SEQ_RANGE = /#{SEQ_NUMBER}:#{SEQ_NUMBER}/n
303
+
304
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
305
+ # ; set of seq-number values, regardless of order.
306
+ # ; Servers MAY coalesce overlaps and/or execute
307
+ # ; the sequence in any order.
308
+ # ; Example: a message sequence number set of
309
+ # ; 2,4:7,9,12:* for a mailbox with 15 messages is
310
+ # ; equivalent to 2,4,5,6,7,9,12,13,14,15
311
+ # ; Example: a message sequence number set of
312
+ # ; *:4,5:7 for a mailbox with 10 messages is
313
+ # ; equivalent to 10,9,8,7,6,5,4,5,6,7 and MAY
314
+ # ; be reordered and overlap coalesced to be
315
+ # ; 4,5,6,7,8,9,10.
316
+ SEQUENCE_SET_ITEM = /#{SEQ_NUMBER}|#{SEQ_RANGE}/n
317
+ SEQUENCE_SET = /#{SEQUENCE_SET_ITEM}(?:,#{SEQUENCE_SET_ITEM})*/n
318
+ SEQUENCE_SET_STR = /\A#{SEQUENCE_SET}\z/n
319
+
320
+ # RFC3501:
321
+ # literal = "{" number "}" CRLF *CHAR8
322
+ # ; Number represents the number of CHAR8s
323
+ # RFC9051:
324
+ # literal = "{" number64 ["+"] "}" CRLF *CHAR8
325
+ # ; <number64> represents the number of CHAR8s.
326
+ # ; A non-synchronizing literal is distinguished
327
+ # ; from a synchronizing literal by the presence of
328
+ # ; "+" before the closing "}".
329
+ # ; Non-synchronizing literals are not allowed when
330
+ # ; sent from server to the client.
331
+ LITERAL = /\{(\d+)\}\r\n/n
332
+
333
+ # RFC3516 (BINARY):
334
+ # literal8 = "~{" number "}" CRLF *OCTET
335
+ # ; <number> represents the number of OCTETs
336
+ # ; in the response string.
337
+ # RFC9051:
338
+ # literal8 = "~{" number64 "}" CRLF *OCTET
339
+ # ; <number64> represents the number of OCTETs
340
+ # ; in the response string.
341
+ LITERAL8 = /~\{(\d+)\}\r\n/n
342
+
343
+ module_function
344
+
345
+ def unescape_quoted!(quoted)
346
+ quoted
347
+ &.gsub!(/\\(#{QUOTED_SPECIALS})/n, "\\1")
348
+ &.force_encoding("UTF-8")
349
+ end
350
+
351
+ def unescape_quoted(quoted)
352
+ quoted
353
+ &.gsub(/\\(#{QUOTED_SPECIALS})/n, "\\1")
354
+ &.force_encoding("UTF-8")
355
+ end
356
+
357
+ end
358
+
359
+ # the default, used in most places
60
360
  BEG_REGEXP = /\G(?:\
61
- (?# 1: SPACE )( +)|\
62
- (?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
63
- (?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
64
- (?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
65
- (?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
66
- (?# 6: LPAR )(\()|\
67
- (?# 7: RPAR )(\))|\
68
- (?# 8: BSLASH )(\\)|\
69
- (?# 9: STAR )(\*)|\
70
- (?# 10: LBRA )(\[)|\
71
- (?# 11: RBRA )(\])|\
72
- (?# 12: LITERAL )\{(\d+)\}\r\n|\
73
- (?# 13: PLUS )(\+)|\
74
- (?# 14: PERCENT )(%)|\
75
- (?# 15: CRLF )(\r\n)|\
76
- (?# 16: EOF )(\z))/ni
77
-
361
+ (?# 1: SPACE )( )|\
362
+ (?# 2: LITERAL8)#{Patterns::LITERAL8}|\
363
+ (?# 3: ATOM prefixed with a compatible subtype)\
364
+ ((?:\
365
+ (?# 4: NIL )(NIL)|\
366
+ (?# 5: NUMBER )(\d+)|\
367
+ (?# 6: PLUS )(\+))\
368
+ (?# 7: ATOM remaining after prefix )(#{Patterns::ATOMISH})?\
369
+ (?# This enables greedy alternation without lookahead, in linear time.)\
370
+ )|\
371
+ (?# Also need to check for ATOM without a subtype prefix.)\
372
+ (?# 8: ATOM )(#{Patterns::ATOMISH})|\
373
+ (?# 9: QUOTED )#{Patterns::QUOTED_rev2}|\
374
+ (?# 10: LPAR )(\()|\
375
+ (?# 11: RPAR )(\))|\
376
+ (?# 12: BSLASH )(\\)|\
377
+ (?# 13: STAR )(\*)|\
378
+ (?# 14: LBRA )(\[)|\
379
+ (?# 15: RBRA )(\])|\
380
+ (?# 16: LITERAL )#{Patterns::LITERAL}|\
381
+ (?# 17: PERCENT )(%)|\
382
+ (?# 18: CRLF )(\r\n)|\
383
+ (?# 19: EOF )(\z))/ni
384
+
385
+ # envelope, body(structure), namespaces
78
386
  DATA_REGEXP = /\G(?:\
79
387
  (?# 1: SPACE )( )|\
80
388
  (?# 2: NIL )(NIL)|\
81
389
  (?# 3: NUMBER )(\d+)|\
82
- (?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
83
- (?# 5: LITERAL )\{(\d+)\}\r\n|\
390
+ (?# 4: QUOTED )#{Patterns::QUOTED_rev2}|\
391
+ (?# 5: LITERAL )#{Patterns::LITERAL}|\
84
392
  (?# 6: LPAR )(\()|\
85
393
  (?# 7: RPAR )(\)))/ni
86
394
 
87
- TEXT_REGEXP = /\G(?:\
88
- (?# 1: TEXT )([^\x00\r\n]*))/ni
395
+ # text, after 'resp-text-code "]"'
396
+ TEXT_REGEXP = /\G(#{Patterns::TEXT_rev2})/n
89
397
 
90
- RTEXT_REGEXP = /\G(?:\
91
- (?# 1: LBRA )(\[)|\
92
- (?# 2: TEXT )([^\x00\r\n]*))/ni
93
-
94
- CTEXT_REGEXP = /\G(?:\
95
- (?# 1: TEXT )([^\x00\r\n\]]*))/ni
398
+ # resp-text-code, after 'atom SP'
399
+ CTEXT_REGEXP = /\G(#{Patterns::CODE_TEXT})/n
96
400
 
97
401
  Token = Struct.new(:symbol, :value)
98
402
 
99
- def response
100
- token = lookahead
101
- case token.symbol
102
- when T_PLUS
103
- result = continue_req
104
- when T_STAR
105
- result = response_untagged
403
+ def_char_matchers :SP, " ", :T_SPACE
404
+ def_char_matchers :PLUS, "+", :T_PLUS
405
+ def_char_matchers :STAR, "*", :T_STAR
406
+
407
+ def_char_matchers :lpar, "(", :T_LPAR
408
+ def_char_matchers :rpar, ")", :T_RPAR
409
+
410
+ def_char_matchers :lbra, "[", :T_LBRA
411
+ def_char_matchers :rbra, "]", :T_RBRA
412
+
413
+ # valid number ranges are not enforced by parser
414
+ # number = 1*DIGIT
415
+ # ; Unsigned 32-bit integer
416
+ # ; (0 <= n < 4,294,967,296)
417
+ def_token_matchers :number, T_NUMBER, coerce: Integer
418
+
419
+ def_token_matchers :quoted, T_QUOTED
420
+
421
+ # string = quoted / literal
422
+ def_token_matchers :string, T_QUOTED, T_LITERAL
423
+
424
+ # used by nstring8 = nstring / literal8
425
+ def_token_matchers :string8, T_QUOTED, T_LITERAL, T_LITERAL8
426
+
427
+ # use where string represents "LABEL" values
428
+ def_token_matchers :case_insensitive__string,
429
+ T_QUOTED, T_LITERAL,
430
+ send: :upcase
431
+
432
+ # n.b: NIL? and NIL! return the "NIL" atom string (truthy) on success.
433
+ # NIL? returns nil when it does *not* match
434
+ def_token_matchers :NIL, T_NIL
435
+
436
+ # In addition to explicitly uses of +tagged-ext-label+, use this to match
437
+ # keywords when the grammar has not provided any extension syntax.
438
+ #
439
+ # Do *not* use this for labels where the grammar specifies extensions
440
+ # can be +atom+, even if all currently defined labels would match. For
441
+ # example response codes in +resp-text-code+.
442
+ #
443
+ # tagged-ext-label = tagged-label-fchar *tagged-label-char
444
+ # ; Is a valid RFC 3501 "atom".
445
+ # tagged-label-fchar = ALPHA / "-" / "_" / "."
446
+ # tagged-label-char = tagged-label-fchar / DIGIT / ":"
447
+ #
448
+ # TODO: add to lexer and only match tagged-ext-label
449
+ def_token_matchers :tagged_ext_label, T_ATOM, T_NIL, send: :upcase
450
+
451
+ def_token_matchers :CRLF, T_CRLF
452
+ def_token_matchers :EOF, T_EOF
453
+
454
+ # atom = 1*ATOM-CHAR
455
+ # ATOM-CHAR = <any CHAR except atom-specials>
456
+ ATOM_TOKENS = [T_ATOM, T_NUMBER, T_NIL, T_LBRA, T_PLUS]
457
+
458
+ SEQUENCE_SET_TOKENS = [T_ATOM, T_NUMBER, T_STAR]
459
+
460
+ # sequence-set = (seq-number / seq-range) ["," sequence-set]
461
+ # sequence-set =/ seq-last-command
462
+ # ; Allow for "result of the last command"
463
+ # ; indicator.
464
+ # seq-last-command = "$"
465
+ #
466
+ # *note*: doesn't match seq-last-command
467
+ def sequence_set
468
+ str = combine_adjacent(*SEQUENCE_SET_TOKENS)
469
+ if Patterns::SEQUENCE_SET_STR.match?(str)
470
+ SequenceSet.new(str)
106
471
  else
107
- result = response_tagged
108
- end
109
- while lookahead.symbol == T_SPACE
110
- # Ignore trailing space for Microsoft Exchange Server
111
- shift_token
472
+ parse_error("unexpected atom %p, expected sequence-set", str)
112
473
  end
113
- match(T_CRLF)
114
- match(T_EOF)
115
- return result
116
474
  end
117
475
 
118
- def continue_req
119
- match(T_PLUS)
120
- token = lookahead
121
- if token.symbol == T_SPACE
122
- shift_token
123
- return ContinuationRequest.new(resp_text, @str)
124
- else
125
- return ContinuationRequest.new(ResponseText.new(nil, ""), @str)
476
+ # ASTRING-CHAR = ATOM-CHAR / resp-specials
477
+ # resp-specials = "]"
478
+ ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA].freeze
479
+
480
+ ASTRING_TOKENS = [T_QUOTED, *ASTRING_CHARS_TOKENS, T_LITERAL].freeze
481
+
482
+ # tag = 1*<any ASTRING-CHAR except "+">
483
+ TAG_TOKENS = (ASTRING_CHARS_TOKENS - [T_PLUS]).freeze
484
+
485
+ # TODO: handle atom, astring_chars, and tag entirely inside the lexer
486
+ def atom; combine_adjacent(*ATOM_TOKENS) end
487
+ def astring_chars; combine_adjacent(*ASTRING_CHARS_TOKENS) end
488
+ def tag; combine_adjacent(*TAG_TOKENS) end
489
+
490
+ # the #accept version of #atom
491
+ def atom?; -combine_adjacent(*ATOM_TOKENS) if lookahead?(*ATOM_TOKENS) end
492
+
493
+ # Returns <tt>atom.upcase</tt>
494
+ def case_insensitive__atom; -combine_adjacent(*ATOM_TOKENS).upcase end
495
+
496
+ # Returns <tt>atom?&.upcase</tt>
497
+ def case_insensitive__atom?
498
+ -combine_adjacent(*ATOM_TOKENS).upcase if lookahead?(*ATOM_TOKENS)
499
+ end
500
+
501
+ # astring = 1*ASTRING-CHAR / string
502
+ def astring
503
+ lookahead?(*ASTRING_CHARS_TOKENS) ? astring_chars : string
504
+ end
505
+
506
+ def astring?
507
+ lookahead?(*ASTRING_CHARS_TOKENS) ? astring_chars : string?
508
+ end
509
+
510
+ # Use #label or #label_in to assert specific known labels
511
+ # (+tagged-ext-label+ only, not +atom+).
512
+ def label(word)
513
+ (val = tagged_ext_label) == word and return val
514
+ parse_error("unexpected atom %p, expected %p instead", val, word)
515
+ end
516
+
517
+ # Use #label or #label_in to assert specific known labels
518
+ # (+tagged-ext-label+ only, not +atom+).
519
+ def label_in(*labels)
520
+ lbl = tagged_ext_label and labels.include?(lbl) and return lbl
521
+ parse_error("unexpected atom %p, expected one of %s instead",
522
+ lbl, labels.join(" or "))
523
+ end
524
+
525
+ # expects "OK" or "PREAUTH" and raises InvalidResponseError on failure
526
+ def resp_cond_auth__name
527
+ lbl = tagged_ext_label and AUTH_CONDS.include? lbl and return lbl
528
+ raise InvalidResponseError, "bad response type %p, expected %s" % [
529
+ lbl, AUTH_CONDS.join(" or ")
530
+ ]
531
+ end
532
+
533
+ # expects "OK" or "NO" or "BAD" and raises InvalidResponseError on failure
534
+ def resp_cond_state__name
535
+ lbl = tagged_ext_label and RESP_COND_STATES.include? lbl and return lbl
536
+ raise InvalidResponseError, "bad response type %p, expected %s" % [
537
+ lbl, RESP_COND_STATES.join(" or ")
538
+ ]
539
+ end
540
+
541
+ # nstring = string / nil
542
+ def nstring
543
+ NIL? ? nil : string
544
+ end
545
+
546
+ def nstring8
547
+ NIL? ? nil : string8
548
+ end
549
+
550
+ def nquoted
551
+ NIL? ? nil : quoted
552
+ end
553
+
554
+ # use where nstring represents "LABEL" values
555
+ def case_insensitive__nstring
556
+ NIL? ? nil : case_insensitive__string
557
+ end
558
+
559
+ # tagged-ext-comp = astring /
560
+ # tagged-ext-comp *(SP tagged-ext-comp) /
561
+ # "(" tagged-ext-comp ")"
562
+ # ; Extensions that follow this general
563
+ # ; syntax should use nstring instead of
564
+ # ; astring when appropriate in the context
565
+ # ; of the extension.
566
+ # ; Note that a message set or a "number"
567
+ # ; can always be represented as an "atom".
568
+ # ; A URL should be represented as
569
+ # ; a "quoted" string.
570
+ def tagged_ext_comp
571
+ vals = []
572
+ while true
573
+ vals << case lookahead!(*ASTRING_TOKENS, T_LPAR).symbol
574
+ when T_LPAR then lpar; ary = tagged_ext_comp; rpar; ary
575
+ when T_NUMBER then number
576
+ else astring
577
+ end
578
+ SP? or break
126
579
  end
580
+ vals
127
581
  end
128
582
 
129
- def response_untagged
130
- match(T_STAR)
131
- match(T_SPACE)
132
- token = lookahead
133
- if token.symbol == T_NUMBER
134
- return numeric_response
135
- elsif token.symbol == T_ATOM
136
- case token.value
137
- when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
138
- return response_cond
139
- when /\A(?:FLAGS)\z/ni
140
- return flags_response
141
- when /\A(?:ID)\z/ni
142
- return id_response
143
- when /\A(?:LIST|LSUB|XLIST)\z/ni
144
- return list_response
145
- when /\A(?:NAMESPACE)\z/ni
146
- return namespace_response
147
- when /\A(?:QUOTA)\z/ni
148
- return getquota_response
149
- when /\A(?:QUOTAROOT)\z/ni
150
- return getquotaroot_response
151
- when /\A(?:ACL)\z/ni
152
- return getacl_response
153
- when /\A(?:SEARCH|SORT)\z/ni
154
- return search_response
155
- when /\A(?:THREAD)\z/ni
156
- return thread_response
157
- when /\A(?:STATUS)\z/ni
158
- return status_response
159
- when /\A(?:CAPABILITY)\z/ni
160
- return capability_response
161
- when /\A(?:NOOP)\z/ni
162
- return ignored_response
163
- else
164
- return text_response
165
- end
583
+ # tagged-ext-simple is a subset of atom
584
+ # TODO: recognize sequence-set in the lexer
585
+ #
586
+ # tagged-ext-simple = sequence-set / number / number64
587
+ def tagged_ext_simple
588
+ number? || sequence_set
589
+ end
590
+
591
+ # tagged-ext-val = tagged-ext-simple /
592
+ # "(" [tagged-ext-comp] ")"
593
+ def tagged_ext_val
594
+ if lpar?
595
+ _ = peek_rpar? ? [] : tagged_ext_comp
596
+ rpar
597
+ _
166
598
  else
167
- parse_error("unexpected token %s", token.symbol)
168
- end
599
+ tagged_ext_simple
600
+ end
601
+ end
602
+
603
+ # mailbox = "INBOX" / astring
604
+ # ; INBOX is case-insensitive. All case variants of
605
+ # ; INBOX (e.g., "iNbOx") MUST be interpreted as INBOX
606
+ # ; not as an astring. An astring which consists of
607
+ # ; the case-insensitive sequence "I" "N" "B" "O" "X"
608
+ # ; is considered to be INBOX and not an astring.
609
+ # ; Refer to section 5.1 for further
610
+ # ; semantic details of mailbox names.
611
+ alias mailbox astring
612
+
613
+ # valid number ranges are not enforced by parser
614
+ # number64 = 1*DIGIT
615
+ # ; Unsigned 63-bit integer
616
+ # ; (0 <= n <= 9,223,372,036,854,775,807)
617
+ alias number64 number
618
+ alias number64? number?
619
+
620
+ # valid number ranges are not enforced by parser
621
+ # nz-number = digit-nz *DIGIT
622
+ # ; Non-zero unsigned 32-bit integer
623
+ # ; (0 < n < 4,294,967,296)
624
+ alias nz_number number
625
+ alias nz_number? number?
626
+
627
+ # valid number ranges are not enforced by parser
628
+ # nz-number64 = digit-nz *DIGIT
629
+ # ; Unsigned 63-bit integer
630
+ # ; (0 < n <= 9,223,372,036,854,775,807)
631
+ alias nz_number64 nz_number
632
+
633
+ # valid number ranges are not enforced by parser
634
+ # uniqueid = nz-number
635
+ # ; Strictly ascending
636
+ alias uniqueid nz_number
637
+
638
+ # valid number ranges are not enforced by parser
639
+ #
640
+ # a 64-bit unsigned integer and is the decimal equivalent for the ID hex
641
+ # string used in the web interface and the Gmail API.
642
+ alias x_gm_id number
643
+
644
+ # [RFC3501 & RFC9051:]
645
+ # response = *(continue-req / response-data) response-done
646
+ #
647
+ # For simplicity, response isn't interpreted as the combination of the
648
+ # three response types, but instead represents any individual server
649
+ # response. Our simplified interpretation is defined as:
650
+ # response = continue-req | response_data | response-tagged
651
+ #
652
+ # n.b: our "response-tagged" definition parses "greeting" too.
653
+ def response
654
+ resp = case lookahead!(T_PLUS, T_STAR, *TAG_TOKENS).symbol
655
+ when T_PLUS then continue_req
656
+ when T_STAR then response_data
657
+ else response_tagged
658
+ end
659
+ accept_spaces # QUIRKY: Ignore trailing space (MS Exchange Server?)
660
+ CRLF!
661
+ EOF!
662
+ resp
663
+ end
664
+
665
+ # RFC3501 & RFC9051:
666
+ # continue-req = "+" SP (resp-text / base64) CRLF
667
+ #
668
+ # n.b: base64 is valid resp-text. And in the spirit of RFC9051 Appx E 23
669
+ # (and to workaround existing servers), we use the following grammar:
670
+ #
671
+ # continue-req = "+" (SP (resp-text)) CRLF
672
+ def continue_req
673
+ PLUS!
674
+ ContinuationRequest.new(SP? ? resp_text : ResponseText::EMPTY, @str)
675
+ end
676
+
677
+ RE_RESPONSE_TYPE = /\G(?:\d+ )?(?<type>#{Patterns::TAGGED_EXT_LABEL})/n
678
+
679
+ # [RFC3501:]
680
+ # response-data = "*" SP (resp-cond-state / resp-cond-bye /
681
+ # mailbox-data / message-data / capability-data) CRLF
682
+ # [RFC4466:]
683
+ # response-data = "*" SP response-payload CRLF
684
+ # response-payload = resp-cond-state / resp-cond-bye /
685
+ # mailbox-data / message-data / capability-data
686
+ # RFC5161 (ENABLE capability):
687
+ # response-data =/ "*" SP enable-data CRLF
688
+ # RFC5255 (LANGUAGE capability)
689
+ # response-payload =/ language-data
690
+ # RFC5255 (I18NLEVEL=1 and I18NLEVEL=2 capabilities)
691
+ # response-payload =/ comparator-data
692
+ # [RFC9051:]
693
+ # response-data = "*" SP (resp-cond-state / resp-cond-bye /
694
+ # mailbox-data / message-data / capability-data /
695
+ # enable-data) CRLF
696
+ #
697
+ # [merging in greeting and response-fatal:]
698
+ # greeting = "*" SP (resp-cond-auth / resp-cond-bye) CRLF
699
+ # response-fatal = "*" SP resp-cond-bye CRLF
700
+ # response-data =/ "*" SP (resp-cond-auth / resp-cond-bye) CRLF
701
+ # [removing duplicates, this is simply]
702
+ # response-payload =/ resp-cond-auth
703
+ #
704
+ # TODO: remove resp-cond-auth and handle greeting separately
705
+ def response_data
706
+ STAR!; SP!
707
+ m = peek_re(RE_RESPONSE_TYPE) or parse_error("unparsable response")
708
+ case m["type"].upcase
709
+ when "OK" then resp_cond_state__untagged # RFC3501, RFC9051
710
+ when "FETCH" then message_data__fetch # RFC3501, RFC9051
711
+ when "EXPUNGE" then message_data__expunge # RFC3501, RFC9051
712
+ when "EXISTS" then mailbox_data__exists # RFC3501, RFC9051
713
+ when "ESEARCH" then esearch_response # RFC4731, RFC9051, etc
714
+ when "VANISHED" then expunged_resp # RFC7162
715
+ when "UIDFETCH" then uidfetch_resp # (draft) UIDONLY
716
+ when "SEARCH" then mailbox_data__search # RFC3501 (obsolete)
717
+ when "CAPABILITY" then capability_data__untagged # RFC3501, RFC9051
718
+ when "FLAGS" then mailbox_data__flags # RFC3501, RFC9051
719
+ when "LIST" then mailbox_data__list # RFC3501, RFC9051
720
+ when "STATUS" then mailbox_data__status # RFC3501, RFC9051
721
+ when "NAMESPACE" then namespace_response # RFC2342, RFC9051
722
+ when "ENABLED" then enable_data # RFC5161, RFC9051
723
+ when "BAD" then resp_cond_state__untagged # RFC3501, RFC9051
724
+ when "NO" then resp_cond_state__untagged # RFC3501, RFC9051
725
+ when "PREAUTH" then resp_cond_auth # RFC3501, RFC9051
726
+ when "BYE" then resp_cond_bye # RFC3501, RFC9051
727
+ when "RECENT" then mailbox_data__recent # RFC3501 (obsolete)
728
+ when "SORT" then sort_data # RFC5256, RFC7162
729
+ when "THREAD" then thread_data # RFC5256
730
+ when "QUOTA" then quota_response # RFC2087, RFC9208
731
+ when "QUOTAROOT" then quotaroot_response # RFC2087, RFC9208
732
+ when "ID" then id_response # RFC2971
733
+ when "ACL" then acl_data # RFC4314
734
+ when "LISTRIGHTS" then listrights_data # RFC4314
735
+ when "MYRIGHTS" then myrights_data # RFC4314
736
+ when "METADATA" then metadata_resp # RFC5464
737
+ when "LANGUAGE" then language_data # RFC5255
738
+ when "COMPARATOR" then comparator_data # RFC5255
739
+ when "CONVERTED" then message_data__converted # RFC5259
740
+ when "LSUB" then mailbox_data__lsub # RFC3501 (obsolete)
741
+ when "XLIST" then mailbox_data__xlist # deprecated
742
+ when "NOOP" then response_data__noop
743
+ else response_data__unhandled
744
+ end
745
+ end
746
+
747
+ def response_data__unhandled(klass = UntaggedResponse)
748
+ num = number?; SP?
749
+ type = tagged_ext_label; SP?
750
+ text = remaining_unparsed
751
+ data =
752
+ if num && text then UnparsedNumericResponseData.new(num, text)
753
+ elsif text then UnparsedData.new(text)
754
+ else num
755
+ end
756
+ klass.new(type, data, @str)
757
+ end
758
+
759
+ # reads all the way up until CRLF
760
+ def remaining_unparsed
761
+ str = @str[@pos...-2] and @pos += str.bytesize
762
+ str&.empty? ? nil : str
169
763
  end
170
764
 
765
+ def response_data__ignored; response_data__unhandled(IgnoredResponse) end
766
+ alias response_data__noop response_data__ignored
767
+
768
+ alias esearch_response response_data__unhandled
769
+ alias expunged_resp response_data__unhandled
770
+ alias uidfetch_resp response_data__unhandled
771
+ alias listrights_data response_data__unhandled
772
+ alias myrights_data response_data__unhandled
773
+ alias metadata_resp response_data__unhandled
774
+ alias language_data response_data__unhandled
775
+ alias comparator_data response_data__unhandled
776
+ alias message_data__converted response_data__unhandled
777
+
778
+ # RFC3501 & RFC9051:
779
+ # response-tagged = tag SP resp-cond-state CRLF
780
+ #
781
+ # resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
782
+ # ; Status condition
783
+ #
784
+ # tag = 1*<any ASTRING-CHAR except "+">
171
785
  def response_tagged
172
- tag = astring_chars
173
- match(T_SPACE)
174
- token = match(T_ATOM)
175
- name = token.value.upcase
176
- match(T_SPACE)
177
- return TaggedResponse.new(tag, name, resp_text, @str)
786
+ tag = tag(); SP!
787
+ name = resp_cond_state__name; SP!
788
+ TaggedResponse.new(tag, name, resp_text, @str)
178
789
  end
179
790
 
180
- def response_cond
181
- token = match(T_ATOM)
182
- name = token.value.upcase
183
- match(T_SPACE)
184
- return UntaggedResponse.new(name, resp_text, @str)
791
+ # RFC3501 & RFC9051:
792
+ # resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
793
+ def resp_cond_state__untagged
794
+ name = resp_cond_state__name; SP!
795
+ UntaggedResponse.new(name, resp_text, @str)
185
796
  end
186
797
 
187
- def numeric_response
188
- n = number
189
- match(T_SPACE)
190
- token = match(T_ATOM)
191
- name = token.value.upcase
192
- case name
193
- when "EXISTS", "RECENT", "EXPUNGE"
194
- return UntaggedResponse.new(name, n, @str)
195
- when "FETCH"
196
- shift_token
197
- match(T_SPACE)
198
- data = FetchData.new(n, msg_att(n))
199
- return UntaggedResponse.new(name, data, @str)
200
- end
798
+ # resp-cond-auth = ("OK" / "PREAUTH") SP resp-text
799
+ def resp_cond_auth
800
+ name = resp_cond_auth__name; SP!
801
+ UntaggedResponse.new(name, resp_text, @str)
802
+ end
803
+
804
+ # resp-cond-bye = "BYE" SP resp-text
805
+ def resp_cond_bye
806
+ name = label(BYE); SP!
807
+ UntaggedResponse.new(name, resp_text, @str)
808
+ end
809
+
810
+ # message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
811
+ def message_data__fetch
812
+ seq = nz_number; SP!
813
+ name = label "FETCH"; SP!
814
+ data = FetchData.new(seq, msg_att(seq))
815
+ UntaggedResponse.new(name, data, @str)
201
816
  end
202
817
 
818
+ def response_data__simple_numeric
819
+ data = nz_number; SP!
820
+ name = tagged_ext_label
821
+ UntaggedResponse.new(name, data, @str)
822
+ end
823
+
824
+ alias message_data__expunge response_data__simple_numeric
825
+ alias mailbox_data__exists response_data__simple_numeric
826
+ alias mailbox_data__recent response_data__simple_numeric
827
+
828
+ # RFC3501 & RFC9051:
829
+ # msg-att = "(" (msg-att-dynamic / msg-att-static)
830
+ # *(SP (msg-att-dynamic / msg-att-static)) ")"
831
+ #
832
+ # msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"
833
+ # RFC5257 (ANNOTATE extension):
834
+ # msg-att-dynamic =/ "ANNOTATION" SP
835
+ # ( "(" entry-att *(SP entry-att) ")" /
836
+ # "(" entry *(SP entry) ")" )
837
+ # RFC7162 (CONDSTORE extension):
838
+ # msg-att-dynamic =/ fetch-mod-resp
839
+ # fetch-mod-resp = "MODSEQ" SP "(" permsg-modsequence ")"
840
+ # RFC8970 (PREVIEW extension):
841
+ # msg-att-dynamic =/ "PREVIEW" SP nstring
842
+ #
843
+ # RFC3501:
844
+ # msg-att-static = "ENVELOPE" SP envelope /
845
+ # "INTERNALDATE" SP date-time /
846
+ # "RFC822" [".HEADER" / ".TEXT"] SP nstring /
847
+ # "RFC822.SIZE" SP number /
848
+ # "BODY" ["STRUCTURE"] SP body /
849
+ # "BODY" section ["<" number ">"] SP nstring /
850
+ # "UID" SP uniqueid
851
+ # RFC3516 (BINARY extension):
852
+ # msg-att-static =/ "BINARY" section-binary SP (nstring / literal8)
853
+ # / "BINARY.SIZE" section-binary SP number
854
+ # RFC8514 (SAVEDATE extension):
855
+ # msg-att-static =/ "SAVEDATE" SP (date-time / nil)
856
+ # RFC8474 (OBJECTID extension):
857
+ # msg-att-static =/ fetch-emailid-resp / fetch-threadid-resp
858
+ # fetch-emailid-resp = "EMAILID" SP "(" objectid ")"
859
+ # fetch-threadid-resp = "THREADID" SP ( "(" objectid ")" / nil )
860
+ # RFC9051:
861
+ # msg-att-static = "ENVELOPE" SP envelope /
862
+ # "INTERNALDATE" SP date-time /
863
+ # "RFC822.SIZE" SP number64 /
864
+ # "BODY" ["STRUCTURE"] SP body /
865
+ # "BODY" section ["<" number ">"] SP nstring /
866
+ # "BINARY" section-binary SP (nstring / literal8) /
867
+ # "BINARY.SIZE" section-binary SP number /
868
+ # "UID" SP uniqueid
869
+ #
870
+ # Re https://www.rfc-editor.org/errata/eid7246, I'm adding "offset" to the
871
+ # official "BINARY" ABNF, like so:
872
+ #
873
+ # msg-att-static =/ "BINARY" section-binary ["<" number ">"] SP
874
+ # (nstring / literal8)
203
875
  def msg_att(n)
204
- match(T_LPAR)
876
+ lpar
205
877
  attr = {}
206
878
  while true
207
- token = lookahead
208
- case token.symbol
209
- when T_RPAR
210
- shift_token
211
- break
212
- when T_SPACE
213
- shift_token
214
- next
215
- end
216
- case token.value
217
- when /\A(?:ENVELOPE)\z/ni
218
- name, val = envelope_data
219
- when /\A(?:FLAGS)\z/ni
220
- name, val = flags_data
221
- when /\A(?:INTERNALDATE)\z/ni
222
- name, val = internaldate_data
223
- when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
224
- name, val = rfc822_text
225
- when /\A(?:RFC822\.SIZE)\z/ni
226
- name, val = rfc822_size
227
- when /\A(?:BODY(?:STRUCTURE)?)\z/ni
228
- name, val = body_data
229
- when /\A(?:UID)\z/ni
230
- name, val = uid_data
231
- when /\A(?:MODSEQ)\z/ni
232
- name, val = modseq_data
233
- else
234
- parse_error("unknown attribute `%s' for {%d}", token.value, n)
235
- end
879
+ name = msg_att__label; SP!
880
+ val =
881
+ case name
882
+ when "UID" then uniqueid
883
+ when "FLAGS" then flag_list
884
+ when "BODY" then body
885
+ when /\ABODY\[/ni then nstring
886
+ when "BODYSTRUCTURE" then body
887
+ when "ENVELOPE" then envelope
888
+ when "INTERNALDATE" then date_time
889
+ when "RFC822.SIZE" then number64
890
+ when /\ABINARY\[/ni then nstring8 # BINARY, IMAP4rev2
891
+ when /\ABINARY\.SIZE\[/ni then number # BINARY, IMAP4rev2
892
+ when "RFC822" then nstring # not in rev2
893
+ when "RFC822.HEADER" then nstring # not in rev2
894
+ when "RFC822.TEXT" then nstring # not in rev2
895
+ when "MODSEQ" then parens__modseq # CONDSTORE
896
+ when "EMAILID" then parens__objectid # OBJECTID
897
+ when "THREADID" then nparens__objectid # OBJECTID
898
+ when "X-GM-MSGID" then x_gm_id # GMail
899
+ when "X-GM-THRID" then x_gm_id # GMail
900
+ when "X-GM-LABELS" then x_gm_labels # GMail
901
+ else parse_error("unknown attribute `%s' for {%d}", name, n)
902
+ end
236
903
  attr[name] = val
904
+ break unless SP?
905
+ break if lookahead_rpar?
237
906
  end
238
- return attr
907
+ rpar
908
+ attr
239
909
  end
240
910
 
241
- def envelope_data
242
- token = match(T_ATOM)
243
- name = token.value.upcase
244
- match(T_SPACE)
245
- return name, envelope
911
+ # appends "[section]" and "<partial>" to the base label
912
+ def msg_att__label
913
+ case (name = tagged_ext_label)
914
+ when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
915
+ # ignoring "[]" fixes https://bugs.ruby-lang.org/issues/5620
916
+ lbra? and rbra
917
+ when "BODY"
918
+ peek_lbra? and name << section and
919
+ peek_str?("<") and name << gt__number__lt # partial
920
+ when "BINARY", "BINARY.SIZE"
921
+ name << section_binary
922
+ # see https://www.rfc-editor.org/errata/eid7246 and the note above
923
+ peek_str?("<") and name << gt__number__lt # partial
924
+ end
925
+ name
246
926
  end
247
927
 
928
+ # this represents the partial size for BODY or BINARY
929
+ alias gt__number__lt atom
930
+
248
931
  def envelope
249
932
  @lex_state = EXPR_DATA
250
933
  token = lookahead
@@ -280,483 +963,364 @@ module Net
280
963
  return result
281
964
  end
282
965
 
283
- def flags_data
284
- token = match(T_ATOM)
285
- name = token.value.upcase
286
- match(T_SPACE)
287
- return name, flag_list
288
- end
289
-
290
- def internaldate_data
291
- token = match(T_ATOM)
292
- name = token.value.upcase
293
- match(T_SPACE)
294
- token = match(T_QUOTED)
295
- return name, token.value
296
- end
297
-
298
- def rfc822_text
299
- token = match(T_ATOM)
300
- name = token.value.upcase
301
- token = lookahead
302
- if token.symbol == T_LBRA
303
- shift_token
304
- match(T_RBRA)
305
- end
306
- match(T_SPACE)
307
- return name, nstring
308
- end
309
-
310
- def rfc822_size
311
- token = match(T_ATOM)
312
- name = token.value.upcase
313
- match(T_SPACE)
314
- return name, number
315
- end
316
-
317
- def body_data
318
- token = match(T_ATOM)
319
- name = token.value.upcase
320
- token = lookahead
321
- if token.symbol == T_SPACE
322
- shift_token
323
- return name, body
324
- end
325
- name.concat(section)
326
- token = lookahead
327
- if token.symbol == T_ATOM
328
- name.concat(token.value)
329
- shift_token
330
- end
331
- match(T_SPACE)
332
- data = nstring
333
- return name, data
334
- end
966
+ # date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
967
+ # SP time SP zone DQUOTE
968
+ alias date_time quoted
969
+ alias ndatetime nquoted
335
970
 
971
+ # RFC-3501 & RFC-9051:
972
+ # body = "(" (body-type-1part / body-type-mpart) ")"
336
973
  def body
337
974
  @lex_state = EXPR_DATA
338
- token = lookahead
339
- if token.symbol == T_NIL
340
- shift_token
341
- result = nil
342
- else
343
- match(T_LPAR)
344
- token = lookahead
345
- if token.symbol == T_LPAR
346
- result = body_type_mpart
347
- else
348
- result = body_type_1part
349
- end
350
- match(T_RPAR)
351
- end
975
+ lpar; result = peek_lpar? ? body_type_mpart : body_type_1part; rpar
976
+ result
977
+ ensure
352
978
  @lex_state = EXPR_BEG
353
- return result
354
979
  end
980
+ alias lookahead_body? lookahead_lpar?
355
981
 
982
+ # RFC-3501 & RFC9051:
983
+ # body-type-1part = (body-type-basic / body-type-msg / body-type-text)
984
+ # [SP body-ext-1part]
356
985
  def body_type_1part
357
- token = lookahead
358
- case token.value
359
- when /\A(?:TEXT)\z/ni
360
- return body_type_text
361
- when /\A(?:MESSAGE)\z/ni
362
- return body_type_msg
363
- when /\A(?:ATTACHMENT)\z/ni
364
- return body_type_attachment
365
- when /\A(?:MIXED)\z/ni
366
- return body_type_mixed
367
- else
368
- return body_type_basic
369
- end
370
- end
371
-
986
+ # This regexp peek is a performance optimization.
987
+ # The lookahead fallback would work fine too.
988
+ m = peek_re(/\G(?:
989
+ (?<TEXT> "TEXT" \s "[^"]+" )
990
+ |(?<MESSAGE> "MESSAGE" \s "(?:RFC822|GLOBAL)" )
991
+ |(?<BASIC> "[^"]+" \s "[^"]+" )
992
+ |(?<MIXED> "MIXED" )
993
+ )/nix)
994
+ choice = m&.named_captures&.compact&.keys&.first
995
+ # In practice, the following line should never be used. But the ABNF
996
+ # *does* allow literals, and this will handle them.
997
+ choice ||= lookahead_case_insensitive__string!
998
+ case choice
999
+ when "BASIC" then body_type_basic # => BodyTypeBasic
1000
+ when "MESSAGE" then body_type_msg # => BodyTypeMessage | BodyTypeBasic
1001
+ when "TEXT" then body_type_text # => BodyTypeText
1002
+ when "MIXED" then body_type_mixed # => BodyTypeMultipart (server bug)
1003
+ else body_type_basic # might be a bug; server's or ours?
1004
+ end
1005
+ end
1006
+
1007
+ # RFC-3501 & RFC9051:
1008
+ # body-type-basic = media-basic SP body-fields
372
1009
  def body_type_basic
373
- mtype, msubtype = media_type
374
- token = lookahead
375
- if token.symbol == T_RPAR
376
- return BodyTypeBasic.new(mtype, msubtype)
377
- end
378
- match(T_SPACE)
379
- param, content_id, desc, enc, size = body_fields
380
- md5, disposition, language, extension = body_ext_1part
381
- return BodyTypeBasic.new(mtype, msubtype,
382
- param, content_id,
383
- desc, enc, size,
384
- md5, disposition, language, extension)
1010
+ type = media_basic # n.b. "basic" type isn't enforced here
1011
+ if lookahead_rpar? then return BodyTypeBasic.new(*type) end # invalid
1012
+ SP!; flds = body_fields
1013
+ SP? and exts = body_ext_1part
1014
+ BodyTypeBasic.new(*type, *flds, *exts)
385
1015
  end
386
1016
 
1017
+ # RFC-3501 & RFC-9051:
1018
+ # body-type-text = media-text SP body-fields SP body-fld-lines
387
1019
  def body_type_text
388
- mtype, msubtype = media_type
389
- match(T_SPACE)
390
- param, content_id, desc, enc, size = body_fields
391
- match(T_SPACE)
392
- lines = number
393
- md5, disposition, language, extension = body_ext_1part
394
- return BodyTypeText.new(mtype, msubtype,
395
- param, content_id,
396
- desc, enc, size,
397
- lines,
398
- md5, disposition, language, extension)
1020
+ type = media_text
1021
+ SP!; flds = body_fields
1022
+ SP!; lines = body_fld_lines
1023
+ SP? and exts = body_ext_1part
1024
+ BodyTypeText.new(*type, *flds, lines, *exts)
399
1025
  end
400
1026
 
1027
+ # RFC-3501 & RFC-9051:
1028
+ # body-type-msg = media-message SP body-fields SP envelope
1029
+ # SP body SP body-fld-lines
401
1030
  def body_type_msg
402
- mtype, msubtype = media_type
403
- match(T_SPACE)
404
- param, content_id, desc, enc, size = body_fields
405
-
406
- token = lookahead
407
- if token.symbol == T_RPAR
408
- # If this is not message/rfc822, we shouldn't apply the RFC822
409
- # spec to it. We should handle anything other than
410
- # message/rfc822 using multipart extension data [rfc3501] (i.e.
411
- # the data itself won't be returned, we would have to retrieve it
412
- # with BODYSTRUCTURE instead of with BODY
413
-
414
- # Also, sometimes a message/rfc822 is included as a large
415
- # attachment instead of having all of the other details
416
- # (e.g. attaching a .eml file to an email)
417
- if msubtype == "RFC822"
418
- return BodyTypeMessage.new(mtype, msubtype, param, content_id,
419
- desc, enc, size, nil, nil, nil, nil,
420
- nil, nil, nil)
421
- else
422
- return BodyTypeExtension.new(mtype, msubtype,
423
- param, content_id,
424
- desc, enc, size)
425
- end
426
- end
427
-
428
- match(T_SPACE)
429
- env = envelope
430
- match(T_SPACE)
431
- b = body
432
- match(T_SPACE)
433
- lines = number
434
- md5, disposition, language, extension = body_ext_1part
435
- return BodyTypeMessage.new(mtype, msubtype,
436
- param, content_id,
437
- desc, enc, size,
438
- env, b, lines,
439
- md5, disposition, language, extension)
440
- end
441
-
442
- def body_type_attachment
443
- mtype = case_insensitive_string
444
- match(T_SPACE)
445
- param = body_fld_param
446
- return BodyTypeAttachment.new(mtype, nil, param)
447
- end
448
-
1031
+ # n.b. "message/rfc822" type isn't enforced here
1032
+ type = media_message
1033
+ SP!; flds = body_fields
1034
+
1035
+ # Sometimes servers send body-type-basic when body-type-msg should be.
1036
+ # E.g: when a message/rfc822 part has "Content-Disposition: attachment".
1037
+ #
1038
+ # * SP "(" --> SP envelope --> continue as body-type-msg
1039
+ # * ")" --> no body-ext-1part --> completed body-type-basic
1040
+ # * SP nstring --> SP body-fld-md5
1041
+ # --> SP body-ext-1part --> continue as body-type-basic
1042
+ #
1043
+ # It's probably better to return BodyTypeBasic---even for
1044
+ # "message/rfc822"---than BodyTypeMessage with invalid fields.
1045
+ unless peek_str?(" (")
1046
+ SP? and exts = body_ext_1part
1047
+ return BodyTypeBasic.new(*type, *flds, *exts)
1048
+ end
1049
+
1050
+ SP!; env = envelope
1051
+ SP!; bdy = body
1052
+ SP!; lines = body_fld_lines
1053
+ SP? and exts = body_ext_1part
1054
+ BodyTypeMessage.new(*type, *flds, env, bdy, lines, *exts)
1055
+ end
1056
+
1057
+ # This is a malformed body-type-mpart with no subparts.
449
1058
  def body_type_mixed
450
- mtype = "MULTIPART"
451
- msubtype = case_insensitive_string
452
- param, disposition, language, extension = body_ext_mpart
453
- return BodyTypeBasic.new(mtype, msubtype, param, nil, nil, nil, nil, nil, disposition, language, extension)
1059
+ # warn "malformed body-type-mpart: multipart/mixed with no parts."
1060
+ type = media_subtype # => "MIXED"
1061
+ SP? and exts = body_ext_mpart
1062
+ BodyTypeMultipart.new("MULTIPART", type, nil, *exts)
454
1063
  end
455
1064
 
1065
+ # RFC-3501 & RFC-9051:
1066
+ # body-type-mpart = 1*body SP media-subtype
1067
+ # [SP body-ext-mpart]
456
1068
  def body_type_mpart
457
- parts = []
458
- while true
459
- token = lookahead
460
- if token.symbol == T_SPACE
461
- shift_token
462
- break
463
- end
464
- parts.push(body)
465
- end
466
- mtype = "MULTIPART"
467
- msubtype = case_insensitive_string
468
- param, disposition, language, extension = body_ext_mpart
469
- return BodyTypeMultipart.new(mtype, msubtype, parts,
470
- param, disposition, language,
471
- extension)
1069
+ parts = [body]; parts << body until SP?; msubtype = media_subtype
1070
+ SP? and exts = body_ext_mpart
1071
+ BodyTypeMultipart.new("MULTIPART", msubtype, parts, *exts)
472
1072
  end
473
1073
 
1074
+ # n.b. this handles both type and subtype
1075
+ #
1076
+ # RFC-3501 vs RFC-9051:
1077
+ # media-basic = ((DQUOTE ("APPLICATION" / "AUDIO" / "IMAGE" /
1078
+ # "MESSAGE" /
1079
+ # "VIDEO") DQUOTE) / string) SP media-subtype
1080
+ # media-basic = ((DQUOTE ("APPLICATION" / "AUDIO" / "IMAGE" /
1081
+ # "FONT" / "MESSAGE" / "MODEL" /
1082
+ # "VIDEO") DQUOTE) / string) SP media-subtype
1083
+ #
1084
+ # media-message = DQUOTE "MESSAGE" DQUOTE SP
1085
+ # DQUOTE "RFC822" DQUOTE
1086
+ # media-message = DQUOTE "MESSAGE" DQUOTE SP
1087
+ # DQUOTE ("RFC822" / "GLOBAL") DQUOTE
1088
+ #
1089
+ # RFC-3501 & RFC-9051:
1090
+ # media-text = DQUOTE "TEXT" DQUOTE SP media-subtype
1091
+ # media-subtype = string
474
1092
  def media_type
475
- mtype = case_insensitive_string
476
- token = lookahead
477
- if token.symbol != T_SPACE
478
- return mtype, nil
479
- end
480
- match(T_SPACE)
481
- msubtype = case_insensitive_string
1093
+ mtype = case_insensitive__string
1094
+ SP? or return mtype, nil # ??? quirky!
1095
+ msubtype = media_subtype
482
1096
  return mtype, msubtype
483
1097
  end
484
1098
 
1099
+ # TODO: check types
1100
+ alias media_basic media_type # */* --- catchall
1101
+ alias media_message media_type # message/rfc822, message/global
1102
+ alias media_text media_type # text/*
1103
+
1104
+ alias media_subtype case_insensitive__string
1105
+
1106
+ # RFC-3501 & RFC-9051:
1107
+ # body-fields = body-fld-param SP body-fld-id SP body-fld-desc SP
1108
+ # body-fld-enc SP body-fld-octets
485
1109
  def body_fields
486
- param = body_fld_param
487
- match(T_SPACE)
488
- content_id = nstring
489
- match(T_SPACE)
490
- desc = nstring
491
- match(T_SPACE)
492
- enc = case_insensitive_string
493
- match(T_SPACE)
494
- size = number
495
- return param, content_id, desc, enc, size
1110
+ fields = []
1111
+ fields << body_fld_param; SP!
1112
+ fields << body_fld_id; SP!
1113
+ fields << body_fld_desc; SP!
1114
+ fields << body_fld_enc; SP!
1115
+ fields << body_fld_octets
1116
+ fields
496
1117
  end
497
1118
 
1119
+ # RFC3501, RFC9051:
1120
+ # body-fld-param = "(" string SP string *(SP string SP string) ")" / nil
498
1121
  def body_fld_param
499
- token = lookahead
500
- if token.symbol == T_NIL
501
- shift_token
502
- return nil
503
- end
504
- match(T_LPAR)
1122
+ return if NIL?
505
1123
  param = {}
506
- while true
507
- token = lookahead
508
- case token.symbol
509
- when T_RPAR
510
- shift_token
511
- break
512
- when T_SPACE
513
- shift_token
514
- end
515
- name = case_insensitive_string
516
- match(T_SPACE)
517
- val = string
518
- param[name] = val
519
- end
520
- return param
521
- end
522
-
1124
+ lpar
1125
+ name = case_insensitive__string; SP!; param[name] = string
1126
+ while SP?
1127
+ name = case_insensitive__string; SP!; param[name] = string
1128
+ end
1129
+ rpar
1130
+ param
1131
+ end
1132
+
1133
+ # RFC2060
1134
+ # body_ext_1part ::= body_fld_md5 [SPACE body_fld_dsp
1135
+ # [SPACE body_fld_lang
1136
+ # [SPACE 1#body_extension]]]
1137
+ # ;; MUST NOT be returned on non-extensible
1138
+ # ;; "BODY" fetch
1139
+ # RFC3501 & RFC9051
1140
+ # body-ext-1part = body-fld-md5 [SP body-fld-dsp [SP body-fld-lang
1141
+ # [SP body-fld-loc *(SP body-extension)]]]
1142
+ # ; MUST NOT be returned on non-extensible
1143
+ # ; "BODY" fetch
523
1144
  def body_ext_1part
524
- token = lookahead
525
- if token.symbol == T_SPACE
526
- shift_token
527
- else
528
- return nil
529
- end
530
- md5 = nstring
531
-
532
- token = lookahead
533
- if token.symbol == T_SPACE
534
- shift_token
535
- else
536
- return md5
537
- end
538
- disposition = body_fld_dsp
539
-
540
- token = lookahead
541
- if token.symbol == T_SPACE
542
- shift_token
543
- else
544
- return md5, disposition
545
- end
546
- language = body_fld_lang
547
-
548
- token = lookahead
549
- if token.symbol == T_SPACE
550
- shift_token
551
- else
552
- return md5, disposition, language
553
- end
554
-
555
- extension = body_extensions
556
- return md5, disposition, language, extension
557
- end
558
-
1145
+ fields = []; fields << body_fld_md5
1146
+ SP? or return fields; fields << body_fld_dsp
1147
+ SP? or return fields; fields << body_fld_lang
1148
+ SP? or return fields; fields << body_fld_loc
1149
+ SP? or return fields; fields << body_extensions
1150
+ fields
1151
+ end
1152
+
1153
+ # RFC-2060:
1154
+ # body_ext_mpart = body_fld_param [SP body_fld_dsp SP body_fld_lang
1155
+ # [SP 1#body_extension]]
1156
+ # ;; MUST NOT be returned on non-extensible
1157
+ # ;; "BODY" fetch
1158
+ # RFC-3501 & RFC-9051:
1159
+ # body-ext-mpart = body-fld-param [SP body-fld-dsp [SP body-fld-lang
1160
+ # [SP body-fld-loc *(SP body-extension)]]]
1161
+ # ; MUST NOT be returned on non-extensible
1162
+ # ; "BODY" fetch
559
1163
  def body_ext_mpart
560
- token = lookahead
561
- if token.symbol == T_SPACE
562
- shift_token
563
- else
564
- return nil
565
- end
566
- param = body_fld_param
567
-
568
- token = lookahead
569
- if token.symbol == T_SPACE
570
- shift_token
571
- else
572
- return param
573
- end
574
- disposition = body_fld_dsp
575
-
576
- token = lookahead
577
- if token.symbol == T_SPACE
578
- shift_token
579
- else
580
- return param, disposition
581
- end
582
- language = body_fld_lang
583
-
584
- token = lookahead
585
- if token.symbol == T_SPACE
586
- shift_token
587
- else
588
- return param, disposition, language
589
- end
590
-
591
- extension = body_extensions
592
- return param, disposition, language, extension
593
- end
594
-
1164
+ fields = []; fields << body_fld_param
1165
+ SP? or return fields; fields << body_fld_dsp
1166
+ SP? or return fields; fields << body_fld_lang
1167
+ SP? or return fields; fields << body_fld_loc
1168
+ SP? or return fields; fields << body_extensions
1169
+ fields
1170
+ end
1171
+
1172
+ alias body_fld_desc nstring
1173
+ alias body_fld_id nstring
1174
+ alias body_fld_loc nstring
1175
+ alias body_fld_lines number64 # number in 3501, number64 in 9051
1176
+ alias body_fld_md5 nstring
1177
+ alias body_fld_octets number
1178
+
1179
+ # RFC-3501 & RFC-9051:
1180
+ # body-fld-enc = (DQUOTE ("7BIT" / "8BIT" / "BINARY" / "BASE64"/
1181
+ # "QUOTED-PRINTABLE") DQUOTE) / string
1182
+ alias body_fld_enc case_insensitive__string
1183
+
1184
+ # body-fld-dsp = "(" string SP body-fld-param ")" / nil
595
1185
  def body_fld_dsp
596
- token = lookahead
597
- if token.symbol == T_NIL
598
- shift_token
599
- return nil
600
- end
601
- match(T_LPAR)
602
- dsp_type = case_insensitive_string
603
- match(T_SPACE)
604
- param = body_fld_param
605
- match(T_RPAR)
606
- return ContentDisposition.new(dsp_type, param)
1186
+ return if NIL?
1187
+ lpar; dsp_type = case_insensitive__string
1188
+ SP!; param = body_fld_param
1189
+ rpar
1190
+ ContentDisposition.new(dsp_type, param)
607
1191
  end
608
1192
 
1193
+ # body-fld-lang = nstring / "(" string *(SP string) ")"
609
1194
  def body_fld_lang
610
- token = lookahead
611
- if token.symbol == T_LPAR
612
- shift_token
613
- result = []
614
- while true
615
- token = lookahead
616
- case token.symbol
617
- when T_RPAR
618
- shift_token
619
- return result
620
- when T_SPACE
621
- shift_token
622
- end
623
- result.push(case_insensitive_string)
624
- end
1195
+ if lpar?
1196
+ result = [case_insensitive__string]
1197
+ result << case_insensitive__string while SP?
1198
+ rpar
1199
+ result
625
1200
  else
626
- lang = nstring
627
- if lang
628
- return lang.upcase
629
- else
630
- return lang
631
- end
1201
+ case_insensitive__nstring
632
1202
  end
633
1203
  end
634
1204
 
1205
+ # body-extension *(SP body-extension)
635
1206
  def body_extensions
636
1207
  result = []
637
- while true
638
- token = lookahead
639
- case token.symbol
640
- when T_RPAR
641
- return result
642
- when T_SPACE
643
- shift_token
644
- end
645
- result.push(body_extension)
646
- end
647
- end
648
-
649
- def body_extension
650
- token = lookahead
651
- case token.symbol
652
- when T_LPAR
653
- shift_token
654
- result = body_extensions
655
- match(T_RPAR)
656
- return result
657
- when T_NUMBER
658
- return number
659
- else
660
- return nstring
661
- end
662
- end
663
-
664
- def section
665
- str = String.new
666
- token = match(T_LBRA)
667
- str.concat(token.value)
668
- token = match(T_ATOM, T_NUMBER, T_RBRA)
669
- if token.symbol == T_RBRA
670
- str.concat(token.value)
671
- return str
672
- end
673
- str.concat(token.value)
674
- token = lookahead
675
- if token.symbol == T_SPACE
676
- shift_token
677
- str.concat(token.value)
678
- token = match(T_LPAR)
679
- str.concat(token.value)
680
- while true
681
- token = lookahead
682
- case token.symbol
683
- when T_RPAR
684
- str.concat(token.value)
685
- shift_token
686
- break
687
- when T_SPACE
688
- shift_token
689
- str.concat(token.value)
690
- end
691
- str.concat(format_string(astring))
692
- end
693
- end
694
- token = match(T_RBRA)
695
- str.concat(token.value)
696
- return str
697
- end
698
-
699
- def format_string(str)
700
- case str
701
- when ""
702
- return '""'
703
- when /[\x80-\xff\r\n]/n
704
- # literal
705
- return "{" + str.bytesize.to_s + "}" + CRLF + str
706
- when /[(){ \x00-\x1f\x7f%*"\\]/n
707
- # quoted string
708
- return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
709
- else
710
- # atom
711
- return str
712
- end
713
- end
714
-
715
- def uid_data
716
- token = match(T_ATOM)
717
- name = token.value.upcase
718
- match(T_SPACE)
719
- return name, number
720
- end
721
-
722
- def modseq_data
723
- token = match(T_ATOM)
724
- name = token.value.upcase
725
- match(T_SPACE)
726
- match(T_LPAR)
727
- modseq = number
728
- match(T_RPAR)
729
- return name, modseq
730
- end
731
-
732
- def ignored_response
733
- while lookahead.symbol != T_CRLF
734
- shift_token
735
- end
736
- return IgnoredResponse.new(@str)
737
- end
738
-
739
- def text_response
740
- token = match(T_ATOM)
741
- name = token.value.upcase
742
- match(T_SPACE)
743
- return UntaggedResponse.new(name, text)
744
- end
745
-
746
- def flags_response
747
- token = match(T_ATOM)
748
- name = token.value.upcase
749
- match(T_SPACE)
750
- return UntaggedResponse.new(name, flag_list, @str)
1208
+ result << body_extension; while SP? do result << body_extension end
1209
+ result
751
1210
  end
752
1211
 
753
- def list_response
754
- token = match(T_ATOM)
755
- name = token.value.upcase
756
- match(T_SPACE)
757
- return UntaggedResponse.new(name, mailbox_list, @str)
1212
+ # body-extension = nstring / number / number64 /
1213
+ # "(" body-extension *(SP body-extension) ")"
1214
+ # ; Future expansion. Client implementations
1215
+ # ; MUST accept body-extension fields. Server
1216
+ # ; implementations MUST NOT generate
1217
+ # ; body-extension fields except as defined by
1218
+ # ; future Standard or Standards Track
1219
+ # ; revisions of this specification.
1220
+ def body_extension
1221
+ if (uint = number64?) then uint
1222
+ elsif lpar? then exts = body_extensions; rpar; exts
1223
+ else nstring
1224
+ end
758
1225
  end
759
1226
 
1227
+ # section = "[" [section-spec] "]"
1228
+ def section
1229
+ str = +lbra
1230
+ str << section_spec unless peek_rbra?
1231
+ str << rbra
1232
+ end
1233
+
1234
+ # section-binary = "[" [section-part] "]"
1235
+ def section_binary
1236
+ str = +lbra
1237
+ str << section_part unless peek_rbra?
1238
+ str << rbra
1239
+ end
1240
+
1241
+ # section-spec = section-msgtext / (section-part ["." section-text])
1242
+ # section-msgtext = "HEADER" /
1243
+ # "HEADER.FIELDS" [".NOT"] SP header-list /
1244
+ # "TEXT"
1245
+ # ; top-level or MESSAGE/RFC822 or
1246
+ # ; MESSAGE/GLOBAL part
1247
+ # section-part = nz-number *("." nz-number)
1248
+ # ; body part reference.
1249
+ # ; Allows for accessing nested body parts.
1250
+ # section-text = section-msgtext / "MIME"
1251
+ # ; text other than actual body part (headers,
1252
+ # ; etc.)
1253
+ #
1254
+ # n.b: we could "cheat" here and just grab all text inside the brackets,
1255
+ # but literals would need special treatment.
1256
+ def section_spec
1257
+ str = "".b
1258
+ str << atom # grabs everything up to "SP header-list" or "]"
1259
+ str << " " << header_list if SP?
1260
+ str
1261
+ end
1262
+
1263
+ # header-list = "(" header-fld-name *(SP header-fld-name) ")"
1264
+ def header_list
1265
+ str = +""
1266
+ str << lpar << header_fld_name
1267
+ str << " " << header_fld_name while SP?
1268
+ str << rpar
1269
+ end
1270
+
1271
+ # section-part = nz-number *("." nz-number)
1272
+ # ; body part reference.
1273
+ # ; Allows for accessing nested body parts.
1274
+ alias section_part atom
1275
+
1276
+ # RFC3501 & RFC9051:
1277
+ # header-fld-name = astring
1278
+ #
1279
+ # NOTE: Previously, Net::IMAP recreated the raw original source string.
1280
+ # Now, it grabs the raw encoded value using @str and @pos. A future
1281
+ # version may simply return the decoded astring value. Although that is
1282
+ # technically incompatible, it should almost never make a difference: all
1283
+ # standard header field names are valid atoms:
1284
+ #
1285
+ # https://www.iana.org/assignments/message-headers/message-headers.xhtml
1286
+ #
1287
+ # Although RFC3501 allows any astring, RFC5322-valid header names are one
1288
+ # or more of the printable US-ASCII characters, except SP and colon. So
1289
+ # empty string isn't valid, and literals aren't needed and should not be
1290
+ # used. This is explicitly unchanged by [I18N-HDRS] (RFC6532).
1291
+ #
1292
+ # RFC5233:
1293
+ # optional-field = field-name ":" unstructured CRLF
1294
+ # field-name = 1*ftext
1295
+ # ftext = %d33-57 / ; Printable US-ASCII
1296
+ # %d59-126 ; characters not including
1297
+ # ; ":".
1298
+ def header_fld_name
1299
+ assert_no_lookahead
1300
+ start = @pos
1301
+ astring
1302
+ @str[start...@pos - 1]
1303
+ end
1304
+
1305
+ # mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
1306
+ # "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
1307
+ # "STATUS" SP mailbox SP "(" [status-att-list] ")" /
1308
+ # number SP "EXISTS" / number SP "RECENT"
1309
+
1310
+ def mailbox_data__flags
1311
+ name = label("FLAGS")
1312
+ SP!
1313
+ UntaggedResponse.new(name, flag_list, @str)
1314
+ end
1315
+
1316
+ def mailbox_data__list
1317
+ name = label_in("LIST", "LSUB", "XLIST")
1318
+ SP!
1319
+ UntaggedResponse.new(name, mailbox_list, @str)
1320
+ end
1321
+ alias mailbox_data__lsub mailbox_data__list
1322
+ alias mailbox_data__xlist mailbox_data__list
1323
+
760
1324
  def mailbox_list
761
1325
  attr = flag_list
762
1326
  match(T_SPACE)
@@ -821,7 +1385,8 @@ module Net
821
1385
  return UntaggedResponse.new(name, data, @str)
822
1386
  end
823
1387
 
824
- def getacl_response
1388
+ # acl-data = "ACL" SP mailbox *(SP identifier SP rights)
1389
+ def acl_data
825
1390
  token = match(T_ATOM)
826
1391
  name = token.value.upcase
827
1392
  match(T_SPACE)
@@ -847,7 +1412,21 @@ module Net
847
1412
  return UntaggedResponse.new(name, data, @str)
848
1413
  end
849
1414
 
850
- def search_response
1415
+ # RFC3501:
1416
+ # mailbox-data = "SEARCH" *(SP nz-number) / ...
1417
+ # RFC5256: SORT
1418
+ # sort-data = "SORT" *(SP nz-number)
1419
+ # RFC7162: CONDSTORE, QRESYNC
1420
+ # mailbox-data =/ "SEARCH" [1*(SP nz-number) SP
1421
+ # search-sort-mod-seq]
1422
+ # sort-data = "SORT" [1*(SP nz-number) SP
1423
+ # search-sort-mod-seq]
1424
+ # ; Updates the SORT response from RFC 5256.
1425
+ # search-sort-mod-seq = "(" "MODSEQ" SP mod-sequence-value ")"
1426
+ # RFC9051:
1427
+ # mailbox-data = obsolete-search-response / ...
1428
+ # obsolete-search-response = "SEARCH" *(SP nz-number)
1429
+ def mailbox_data__search
851
1430
  token = match(T_ATOM)
852
1431
  name = token.value.upcase
853
1432
  token = lookahead
@@ -877,8 +1456,9 @@ module Net
877
1456
  end
878
1457
  return UntaggedResponse.new(name, data, @str)
879
1458
  end
1459
+ alias sort_data mailbox_data__search
880
1460
 
881
- def thread_response
1461
+ def thread_data
882
1462
  token = match(T_ATOM)
883
1463
  name = token.value.upcase
884
1464
  token = lookahead
@@ -940,56 +1520,116 @@ module Net
940
1520
  return rootmember
941
1521
  end
942
1522
 
943
- def status_response
944
- token = match(T_ATOM)
945
- name = token.value.upcase
946
- match(T_SPACE)
947
- mailbox = astring
948
- match(T_SPACE)
949
- match(T_LPAR)
950
- attr = {}
951
- while true
952
- token = lookahead
953
- case token.symbol
954
- when T_RPAR
955
- shift_token
956
- break
957
- when T_SPACE
958
- shift_token
1523
+ # mailbox-data =/ "STATUS" SP mailbox SP "(" [status-att-list] ")"
1524
+ def mailbox_data__status
1525
+ resp_name = label("STATUS"); SP!
1526
+ mbox_name = mailbox; SP!
1527
+ lpar; attr = status_att_list; rpar
1528
+ UntaggedResponse.new(resp_name, StatusData.new(mbox_name, attr), @str)
1529
+ end
1530
+
1531
+ # RFC3501
1532
+ # status-att-list = status-att SP number *(SP status-att SP number)
1533
+ # RFC4466, RFC9051, and RFC3501 Errata
1534
+ # status-att-list = status-att-val *(SP status-att-val)
1535
+ def status_att_list
1536
+ attrs = [status_att_val]
1537
+ while SP? do attrs << status_att_val end
1538
+ attrs.to_h
1539
+ end
1540
+
1541
+ # RFC3501 Errata:
1542
+ # status-att-val = ("MESSAGES" SP number) / ("RECENT" SP number) /
1543
+ # ("UIDNEXT" SP nz-number) / ("UIDVALIDITY" SP nz-number) /
1544
+ # ("UNSEEN" SP number)
1545
+ # RFC4466:
1546
+ # status-att-val = ("MESSAGES" SP number) /
1547
+ # ("RECENT" SP number) /
1548
+ # ("UIDNEXT" SP nz-number) /
1549
+ # ("UIDVALIDITY" SP nz-number) /
1550
+ # ("UNSEEN" SP number)
1551
+ # ;; Extensions to the STATUS responses
1552
+ # ;; should extend this production.
1553
+ # ;; Extensions should use the generic
1554
+ # ;; syntax defined by tagged-ext.
1555
+ # RFC9051:
1556
+ # status-att-val = ("MESSAGES" SP number) /
1557
+ # ("UIDNEXT" SP nz-number) /
1558
+ # ("UIDVALIDITY" SP nz-number) /
1559
+ # ("UNSEEN" SP number) /
1560
+ # ("DELETED" SP number) /
1561
+ # ("SIZE" SP number64)
1562
+ # ; Extensions to the STATUS responses
1563
+ # ; should extend this production.
1564
+ # ; Extensions should use the generic
1565
+ # ; syntax defined by tagged-ext.
1566
+ # RFC7162:
1567
+ # status-att-val =/ "HIGHESTMODSEQ" SP mod-sequence-valzer
1568
+ # ;; Extends non-terminal defined in [RFC4466].
1569
+ # ;; Value 0 denotes that the mailbox doesn't
1570
+ # ;; support persistent mod-sequences
1571
+ # ;; as described in Section 3.1.2.2.
1572
+ # RFC7889:
1573
+ # status-att-val =/ "APPENDLIMIT" SP (number / nil)
1574
+ # ;; status-att-val is defined in RFC 4466
1575
+ # RFC8438:
1576
+ # status-att-val =/ "SIZE" SP number64
1577
+ # RFC8474:
1578
+ # status-att-val =/ "MAILBOXID" SP "(" objectid ")"
1579
+ # ; follows tagged-ext production from [RFC4466]
1580
+ def status_att_val
1581
+ key = tagged_ext_label
1582
+ SP!
1583
+ val =
1584
+ case key
1585
+ when "MESSAGES" then number # RFC3501, RFC9051
1586
+ when "UNSEEN" then number # RFC3501, RFC9051
1587
+ when "DELETED" then number # RFC3501, RFC9051
1588
+ when "UIDNEXT" then nz_number # RFC3501, RFC9051
1589
+ when "UIDVALIDITY" then nz_number # RFC3501, RFC9051
1590
+ when "RECENT" then number # RFC3501 (obsolete)
1591
+ when "SIZE" then number64 # RFC8483, RFC9051
1592
+ when "MAILBOXID" then parens__objectid # RFC8474
1593
+ else
1594
+ number? || ExtensionData.new(tagged_ext_val)
959
1595
  end
960
- token = match(T_ATOM)
961
- key = token.value.upcase
962
- match(T_SPACE)
963
- val = number
964
- attr[key] = val
965
- end
966
- data = StatusData.new(mailbox, attr)
967
- return UntaggedResponse.new(name, data, @str)
1596
+ [key, val]
968
1597
  end
969
1598
 
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)
1599
+ # The presence of "IMAP4rev1" or "IMAP4rev2" is unenforced here.
1600
+ # The grammar rule is used by both response-data and resp-text-code.
1601
+ # But this method only returns UntaggedResponse (response-data).
1602
+ #
1603
+ # RFC3501:
1604
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1"
1605
+ # *(SP capability)
1606
+ # RFC9051:
1607
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev2"
1608
+ # *(SP capability)
1609
+ def capability_data__untagged
1610
+ UntaggedResponse.new label("CAPABILITY"), capability__list, @str
975
1611
  end
976
1612
 
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
1613
+ # enable-data = "ENABLED" *(SP capability)
1614
+ def enable_data
1615
+ UntaggedResponse.new label("ENABLED"), capability__list, @str
1616
+ end
1617
+
1618
+ # As a workaround for buggy servers, allow a trailing SP:
1619
+ # *(SP capability) [SP]
1620
+ def capability__list
1621
+ list = []; while SP? && (capa = capability?) do list << capa end; list
991
1622
  end
992
1623
 
1624
+ alias resp_code__capability capability__list
1625
+
1626
+ # capability = ("AUTH=" auth-type) / atom
1627
+ # ; New capabilities MUST begin with "X" or be
1628
+ # ; registered with IANA as standard or
1629
+ # ; standards-track
1630
+ alias capability case_insensitive__atom
1631
+ alias capability? case_insensitive__atom?
1632
+
993
1633
  def id_response
994
1634
  token = match(T_ATOM)
995
1635
  name = token.value.upcase
@@ -1019,147 +1659,181 @@ module Net
1019
1659
  end
1020
1660
  end
1021
1661
 
1662
+ # namespace-response = "NAMESPACE" SP namespace
1663
+ # SP namespace SP namespace
1664
+ # ; The first Namespace is the Personal Namespace(s).
1665
+ # ; The second Namespace is the Other Users'
1666
+ # ; Namespace(s).
1667
+ # ; The third Namespace is the Shared Namespace(s).
1022
1668
  def namespace_response
1669
+ name = label("NAMESPACE")
1023
1670
  @lex_state = EXPR_DATA
1024
- token = lookahead
1025
- token = match(T_ATOM)
1026
- name = token.value.upcase
1027
- match(T_SPACE)
1028
- personal = namespaces
1029
- match(T_SPACE)
1030
- other = namespaces
1031
- match(T_SPACE)
1032
- shared = namespaces
1671
+ data = Namespaces.new((SP!; namespace),
1672
+ (SP!; namespace),
1673
+ (SP!; namespace))
1674
+ UntaggedResponse.new(name, data, @str)
1675
+ ensure
1033
1676
  @lex_state = EXPR_BEG
1034
- data = Namespaces.new(personal, other, shared)
1035
- return UntaggedResponse.new(name, data, @str)
1036
- end
1037
-
1038
- def namespaces
1039
- token = lookahead
1040
- # empty () is not allowed, so nil is functionally identical to empty.
1041
- data = []
1042
- if token.symbol == T_NIL
1043
- shift_token
1044
- else
1045
- match(T_LPAR)
1046
- loop do
1047
- data << namespace
1048
- break unless lookahead.symbol == T_SPACE
1049
- shift_token
1050
- end
1051
- match(T_RPAR)
1052
- end
1053
- data
1054
1677
  end
1055
1678
 
1679
+ # namespace = nil / "(" 1*namespace-descr ")"
1056
1680
  def namespace
1057
- match(T_LPAR)
1058
- prefix = match(T_QUOTED, T_LITERAL).value
1059
- match(T_SPACE)
1060
- delimiter = string
1681
+ NIL? and return []
1682
+ lpar
1683
+ list = [namespace_descr]
1684
+ list << namespace_descr until rpar?
1685
+ list
1686
+ end
1687
+
1688
+ # namespace-descr = "(" string SP
1689
+ # (DQUOTE QUOTED-CHAR DQUOTE / nil)
1690
+ # [namespace-response-extensions] ")"
1691
+ def namespace_descr
1692
+ lpar
1693
+ prefix = string; SP!
1694
+ delimiter = nquoted # n.b: should only accept single char
1061
1695
  extensions = namespace_response_extensions
1062
- match(T_RPAR)
1696
+ rpar
1063
1697
  Namespace.new(prefix, delimiter, extensions)
1064
1698
  end
1065
1699
 
1700
+ # namespace-response-extensions = *namespace-response-extension
1701
+ # namespace-response-extension = SP string SP
1702
+ # "(" string *(SP string) ")"
1066
1703
  def namespace_response_extensions
1067
1704
  data = {}
1068
- token = lookahead
1069
- if token.symbol == T_SPACE
1070
- shift_token
1071
- name = match(T_QUOTED, T_LITERAL).value
1705
+ while SP?
1706
+ name = string; SP!
1707
+ lpar
1072
1708
  data[name] ||= []
1073
- match(T_SPACE)
1074
- match(T_LPAR)
1075
- loop do
1076
- data[name].push match(T_QUOTED, T_LITERAL).value
1077
- break unless lookahead.symbol == T_SPACE
1078
- shift_token
1079
- end
1080
- match(T_RPAR)
1709
+ data[name] << string
1710
+ data[name] << string while SP?
1711
+ rpar
1081
1712
  end
1082
1713
  data
1083
1714
  end
1084
1715
 
1085
- # text = 1*TEXT-CHAR
1086
- # TEXT-CHAR = <any CHAR except CR and LF>
1716
+ # TEXT-CHAR = <any CHAR except CR and LF>
1717
+ # RFC3501:
1718
+ # text = 1*TEXT-CHAR
1719
+ # RFC9051:
1720
+ # text = 1*(TEXT-CHAR / UTF8-2 / UTF8-3 / UTF8-4)
1721
+ # ; Non-ASCII text can only be returned
1722
+ # ; after ENABLE IMAP4rev2 command
1087
1723
  def text
1088
- match(T_TEXT, lex_state: EXPR_TEXT).value
1724
+ match_re(TEXT_REGEXP, "text")[0].force_encoding("UTF-8")
1725
+ end
1726
+
1727
+ # an "accept" versiun of #text
1728
+ def text?
1729
+ accept_re(TEXT_REGEXP)&.[](0)&.force_encoding("UTF-8")
1089
1730
  end
1090
1731
 
1091
- # resp-text = ["[" resp-text-code "]" SP] text
1732
+ # RFC3501:
1733
+ # resp-text = ["[" resp-text-code "]" SP] text
1734
+ # RFC9051:
1735
+ # resp-text = ["[" resp-text-code "]" SP] [text]
1736
+ #
1737
+ # We leniently re-interpret this as
1738
+ # resp-text = ["[" resp-text-code "]" [SP [text]] / [text]
1092
1739
  def resp_text
1093
- token = match(T_LBRA, T_TEXT, lex_state: EXPR_RTEXT)
1094
- case token.symbol
1095
- when T_LBRA
1096
- code = resp_text_code
1097
- match(T_RBRA)
1098
- accept_space # violating RFC
1099
- ResponseText.new(code, text)
1100
- when T_TEXT
1101
- ResponseText.new(nil, token.value)
1740
+ if lbra?
1741
+ code = resp_text_code; rbra
1742
+ ResponseText.new(code, SP? && text? || "")
1743
+ else
1744
+ ResponseText.new(nil, text? || "")
1102
1745
  end
1103
1746
  end
1104
1747
 
1105
- # See https://www.rfc-editor.org/errata/rfc3501
1748
+ # RFC3501 (See https://www.rfc-editor.org/errata/rfc3501):
1749
+ # resp-text-code = "ALERT" /
1750
+ # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
1751
+ # capability-data / "PARSE" /
1752
+ # "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
1753
+ # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1754
+ # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
1755
+ # "UNSEEN" SP nz-number /
1756
+ # atom [SP 1*<any TEXT-CHAR except "]">]
1757
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1"
1758
+ # *(SP capability)
1106
1759
  #
1107
- # resp-text-code = "ALERT" /
1108
- # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
1109
- # capability-data / "PARSE" /
1110
- # "PERMANENTFLAGS" SP "("
1111
- # [flag-perm *(SP flag-perm)] ")" /
1112
- # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1113
- # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
1114
- # "UNSEEN" SP nz-number /
1115
- # atom [SP 1*<any TEXT-CHAR except "]">]
1760
+ # RFC5530:
1761
+ # resp-text-code =/ "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
1762
+ # "AUTHORIZATIONFAILED" / "EXPIRED" /
1763
+ # "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
1764
+ # "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
1765
+ # "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
1766
+ # "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
1767
+ # "NONEXISTENT"
1768
+ # RFC9051:
1769
+ # resp-text-code = "ALERT" /
1770
+ # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
1771
+ # capability-data / "PARSE" /
1772
+ # "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
1773
+ # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1774
+ # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
1775
+ # resp-code-apnd / resp-code-copy / "UIDNOTSTICKY" /
1776
+ # "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
1777
+ # "AUTHORIZATIONFAILED" / "EXPIRED" /
1778
+ # "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
1779
+ # "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
1780
+ # "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
1781
+ # "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
1782
+ # "NONEXISTENT" / "NOTSAVED" / "HASCHILDREN" /
1783
+ # "CLOSED" /
1784
+ # "UNKNOWN-CTE" /
1785
+ # atom [SP 1*<any TEXT-CHAR except "]">]
1786
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev2"
1787
+ # *(SP capability)
1116
1788
  #
1117
- # +UIDPLUS+ ABNF:: https://www.rfc-editor.org/rfc/rfc4315.html#section-4
1118
- # resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
1789
+ # RFC4315 (UIDPLUS), RFC9051 (IMAP4rev2):
1790
+ # resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
1791
+ # resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
1792
+ # resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
1793
+ #
1794
+ # RFC7162 (CONDSTORE):
1795
+ # resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
1796
+ # "NOMODSEQ" /
1797
+ # "MODIFIED" SP sequence-set
1798
+ #
1799
+ # RFC8474: OBJECTID
1800
+ # resp-text-code =/ "MAILBOXID" SP "(" objectid ")"
1119
1801
  def resp_text_code
1120
- token = match(T_ATOM)
1121
- name = token.value.upcase
1122
- case name
1123
- when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
1124
- result = ResponseCode.new(name, nil)
1125
- when /\A(?:BADCHARSET)\z/n
1126
- result = ResponseCode.new(name, charset_list)
1127
- when /\A(?:CAPABILITY)\z/ni
1128
- result = ResponseCode.new(name, capability_data)
1129
- when /\A(?:PERMANENTFLAGS)\z/n
1130
- match(T_SPACE)
1131
- result = ResponseCode.new(name, flag_list)
1132
- when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
1133
- match(T_SPACE)
1134
- result = ResponseCode.new(name, number)
1135
- when /\A(?:APPENDUID)\z/n
1136
- result = ResponseCode.new(name, resp_code_apnd__data)
1137
- when /\A(?:COPYUID)\z/n
1138
- result = ResponseCode.new(name, resp_code_copy__data)
1139
- else
1140
- token = lookahead
1141
- if token.symbol == T_SPACE
1142
- shift_token
1143
- token = match(T_TEXT, lex_state: EXPR_CTEXT)
1144
- result = ResponseCode.new(name, token.value)
1802
+ name = resp_text_code__name
1803
+ data =
1804
+ case name
1805
+ when "CAPABILITY" then resp_code__capability
1806
+ when "PERMANENTFLAGS" then SP? ? flag_perm__list : []
1807
+ when "UIDNEXT" then SP!; nz_number
1808
+ when "UIDVALIDITY" then SP!; nz_number
1809
+ when "UNSEEN" then SP!; nz_number # rev1 only
1810
+ when "APPENDUID" then SP!; resp_code_apnd__data # rev2, UIDPLUS
1811
+ when "COPYUID" then SP!; resp_code_copy__data # rev2, UIDPLUS
1812
+ when "BADCHARSET" then SP? ? charset__list : []
1813
+ when "ALERT", "PARSE", "READ-ONLY", "READ-WRITE", "TRYCREATE",
1814
+ "UNAVAILABLE", "AUTHENTICATIONFAILED", "AUTHORIZATIONFAILED",
1815
+ "EXPIRED", "PRIVACYREQUIRED", "CONTACTADMIN", "NOPERM", "INUSE",
1816
+ "EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT",
1817
+ "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
1818
+ "NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
1819
+ when "NOMODSEQ" # CONDSTORE
1820
+ when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1145
1821
  else
1146
- result = ResponseCode.new(name, nil)
1822
+ SP? and text_chars_except_rbra
1147
1823
  end
1148
- end
1149
- return result
1824
+ ResponseCode.new(name, data)
1150
1825
  end
1151
1826
 
1152
- def charset_list
1153
- result = []
1154
- if accept(T_SPACE)
1155
- match(T_LPAR)
1156
- result << charset
1157
- while accept(T_SPACE)
1158
- result << charset
1159
- end
1160
- match(T_RPAR)
1161
- end
1162
- result
1827
+ alias resp_text_code__name case_insensitive__atom
1828
+
1829
+ # 1*<any TEXT-CHAR except "]">
1830
+ def text_chars_except_rbra
1831
+ match_re(CTEXT_REGEXP, '1*<any TEXT-CHAR except "]">')[0]
1832
+ end
1833
+
1834
+ # "(" charset *(SP charset) ")"
1835
+ def charset__list
1836
+ lpar; list = [charset]; while SP? do list << charset end; rpar; list
1163
1837
  end
1164
1838
 
1165
1839
  # already matched: "APPENDUID"
@@ -1175,8 +1849,8 @@ module Net
1175
1849
  # match uid_set even if that returns a single-member array.
1176
1850
  #
1177
1851
  def resp_code_apnd__data
1178
- match(T_SPACE); validity = number
1179
- match(T_SPACE); dst_uids = uid_set # uniqueid ⊂ uid-set
1852
+ validity = number; SP!
1853
+ dst_uids = uid_set # uniqueid ⊂ uid-set
1180
1854
  UIDPlusData.new(validity, nil, dst_uids)
1181
1855
  end
1182
1856
 
@@ -1184,9 +1858,9 @@ module Net
1184
1858
  #
1185
1859
  # resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
1186
1860
  def resp_code_copy__data
1187
- match(T_SPACE); validity = number
1188
- match(T_SPACE); src_uids = uid_set
1189
- match(T_SPACE); dst_uids = uid_set
1861
+ validity = number; SP!
1862
+ src_uids = uid_set; SP!
1863
+ dst_uids = uid_set
1190
1864
  UIDPlusData.new(validity, src_uids, dst_uids)
1191
1865
  end
1192
1866
 
@@ -1230,9 +1904,7 @@ module Net
1230
1904
  mailbox = $3
1231
1905
  host = $4
1232
1906
  for s in [name, route, mailbox, host]
1233
- if s
1234
- s.gsub!(/\\(["\\])/n, "\\1")
1235
- end
1907
+ Patterns.unescape_quoted! s
1236
1908
  end
1237
1909
  else
1238
1910
  name = nstring
@@ -1247,124 +1919,78 @@ module Net
1247
1919
  return Address.new(name, route, mailbox, host)
1248
1920
  end
1249
1921
 
1250
- FLAG_REGEXP = /\
1251
- (?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
1252
- (?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
1253
-
1922
+ # flag-list = "(" [flag *(SP flag)] ")"
1254
1923
  def flag_list
1255
- if @str.index(/\(([^)]*)\)/ni, @pos)
1256
- @pos = $~.end(0)
1257
- return $1.scan(FLAG_REGEXP).collect { |flag, atom|
1258
- if atom
1259
- atom
1260
- else
1261
- flag.capitalize.intern
1262
- end
1263
- }
1264
- else
1265
- parse_error("invalid flag list")
1266
- end
1267
- end
1268
-
1269
- def nstring
1270
- token = lookahead
1271
- if token.symbol == T_NIL
1272
- shift_token
1273
- return nil
1274
- else
1275
- return string
1276
- end
1277
- end
1278
-
1279
- def astring
1280
- token = lookahead
1281
- if string_token?(token)
1282
- return string
1283
- else
1284
- return astring_chars
1285
- end
1286
- end
1287
-
1288
- def string
1289
- token = lookahead
1290
- if token.symbol == T_NIL
1291
- shift_token
1292
- return nil
1293
- end
1294
- token = match(T_QUOTED, T_LITERAL)
1295
- return token.value
1296
- end
1297
-
1298
- STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
1299
-
1300
- def string_token?(token)
1301
- return STRING_TOKENS.include?(token.symbol)
1302
- end
1303
-
1304
- def case_insensitive_string
1305
- token = lookahead
1306
- if token.symbol == T_NIL
1307
- shift_token
1308
- return nil
1309
- end
1310
- token = match(T_QUOTED, T_LITERAL)
1311
- return token.value.upcase
1924
+ match_re(Patterns::FLAG_LIST, "flag-list")[1]
1925
+ .split(nil)
1926
+ .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1927
+ end
1928
+
1929
+ # "(" [flag-perm *(SP flag-perm)] ")"
1930
+ def flag_perm__list
1931
+ match_re(Patterns::FLAG_PERM_LIST, "PERMANENTFLAGS flag-perm list")[1]
1932
+ .split(nil)
1933
+ .map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
1934
+ end
1935
+
1936
+ # Not checking for max one mbx-list-sflag in the parser.
1937
+ # >>>
1938
+ # mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
1939
+ # *(SP mbx-list-oflag) /
1940
+ # mbx-list-oflag *(SP mbx-list-oflag)
1941
+ # mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
1942
+ # "\Subscribed" / "\Remote" / flag-extension
1943
+ # ; Other flags; multiple from this list are
1944
+ # ; possible per LIST response, but each flag
1945
+ # ; can only appear once per LIST response
1946
+ # mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
1947
+ # "\Unmarked"
1948
+ # ; Selectability flags; only one per LIST response
1949
+ def parens__mbx_list_flags
1950
+ match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
1951
+ .split(nil).map! { _1.capitalize.to_sym }
1952
+ end
1953
+
1954
+ # See https://developers.google.com/gmail/imap/imap-extensions
1955
+ def x_gm_label; accept(T_BSLASH) ? atom.capitalize.to_sym : astring end
1956
+
1957
+ # See https://developers.google.com/gmail/imap/imap-extensions
1958
+ def x_gm_labels
1959
+ lpar; return [] if rpar?
1960
+ labels = []
1961
+ labels << x_gm_label
1962
+ labels << x_gm_label while SP?
1963
+ rpar
1964
+ labels
1312
1965
  end
1313
1966
 
1314
- # atom = 1*ATOM-CHAR
1315
- # ATOM-CHAR = <any CHAR except atom-specials>
1316
- ATOM_TOKENS = [
1317
- T_ATOM,
1318
- T_NUMBER,
1319
- T_NIL,
1320
- T_LBRA,
1321
- T_PLUS
1322
- ]
1323
-
1324
- def atom
1325
- -combine_adjacent(*ATOM_TOKENS)
1326
- end
1967
+ # See https://www.rfc-editor.org/errata/rfc3501
1968
+ #
1969
+ # charset = atom / quoted
1970
+ def charset; quoted? || atom end
1327
1971
 
1328
- # ASTRING-CHAR = ATOM-CHAR / resp-specials
1329
- # resp-specials = "]"
1330
- ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA]
1972
+ # RFC7162:
1973
+ # mod-sequence-value = 1*DIGIT
1974
+ # ;; Positive unsigned 63-bit integer
1975
+ # ;; (mod-sequence)
1976
+ # ;; (1 <= n <= 9,223,372,036,854,775,807).
1977
+ alias mod_sequence_value nz_number64
1331
1978
 
1332
- def astring_chars
1333
- combine_adjacent(*ASTRING_CHARS_TOKENS)
1334
- end
1979
+ # RFC7162:
1980
+ # permsg-modsequence = mod-sequence-value
1981
+ # ;; Per-message mod-sequence.
1982
+ alias permsg_modsequence mod_sequence_value
1335
1983
 
1336
- def combine_adjacent(*tokens)
1337
- result = "".b
1338
- while token = accept(*tokens)
1339
- result << token.value
1340
- end
1341
- if result.empty?
1342
- parse_error('unexpected token %s (expected %s)',
1343
- lookahead.symbol, args.join(" or "))
1344
- end
1345
- result
1346
- end
1984
+ def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
1347
1985
 
1348
- # See https://www.rfc-editor.org/errata/rfc3501
1349
- #
1350
- # charset = atom / quoted
1351
- def charset
1352
- if token = accept(T_QUOTED)
1353
- token.value
1354
- else
1355
- atom
1356
- end
1357
- end
1986
+ # RFC8474:
1987
+ # objectid = 1*255(ALPHA / DIGIT / "_" / "-")
1988
+ # ; characters in object identifiers are case
1989
+ # ; significant
1990
+ alias objectid atom
1358
1991
 
1359
- def number
1360
- token = lookahead
1361
- if token.symbol == T_NIL
1362
- shift_token
1363
- return nil
1364
- end
1365
- token = match(T_NUMBER)
1366
- return token.value.to_i
1367
- end
1992
+ def parens__objectid; lpar; _ = objectid; rpar; _ end
1993
+ def nparens__objectid; NIL? ? nil : parens__objectid end
1368
1994
 
1369
1995
  # RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
1370
1996
  # uid-set = (uniqueid / uid-range) *("," uid-set)
@@ -1393,64 +2019,15 @@ module Net
1393
2019
 
1394
2020
  SPACES_REGEXP = /\G */n
1395
2021
 
1396
- # This advances @pos directly so it's safe before changing @lex_state.
1397
- def accept_space
1398
- if @token
1399
- shift_token if @token.symbol == T_SPACE
1400
- elsif @str[@pos] == " "
1401
- @pos += 1
1402
- end
1403
- end
1404
-
1405
2022
  # The RFC is very strict about this and usually we should be too.
1406
2023
  # But skipping spaces is usually a safe workaround for buggy servers.
1407
2024
  #
1408
2025
  # This advances @pos directly so it's safe before changing @lex_state.
1409
2026
  def accept_spaces
1410
- shift_token if @token&.symbol == T_SPACE
1411
- if @str.index(SPACES_REGEXP, @pos)
2027
+ return false unless SP?
2028
+ @str.index(SPACES_REGEXP, @pos) and
1412
2029
  @pos = $~.end(0)
1413
- end
1414
- end
1415
-
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
2030
+ true
1454
2031
  end
1455
2032
 
1456
2033
  def next_token
@@ -1461,38 +2038,46 @@ module Net
1461
2038
  if $1
1462
2039
  return Token.new(T_SPACE, $+)
1463
2040
  elsif $2
1464
- return Token.new(T_NIL, $+)
1465
- elsif $3
1466
- return Token.new(T_NUMBER, $+)
2041
+ len = $+.to_i
2042
+ val = @str[@pos, len]
2043
+ @pos += len
2044
+ return Token.new(T_LITERAL8, val)
2045
+ elsif $3 && $7
2046
+ # greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
2047
+ return Token.new(T_ATOM, $3)
1467
2048
  elsif $4
1468
- return Token.new(T_ATOM, $+)
2049
+ return Token.new(T_NIL, $+)
1469
2050
  elsif $5
1470
- return Token.new(T_QUOTED,
1471
- $+.gsub(/\\(["\\])/n, "\\1"))
2051
+ return Token.new(T_NUMBER, $+)
1472
2052
  elsif $6
2053
+ return Token.new(T_PLUS, $+)
2054
+ elsif $8
2055
+ # match ATOM, without a NUMBER, NIL, or PLUS prefix
2056
+ return Token.new(T_ATOM, $+)
2057
+ elsif $9
2058
+ return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
2059
+ elsif $10
1473
2060
  return Token.new(T_LPAR, $+)
1474
- elsif $7
2061
+ elsif $11
1475
2062
  return Token.new(T_RPAR, $+)
1476
- elsif $8
2063
+ elsif $12
1477
2064
  return Token.new(T_BSLASH, $+)
1478
- elsif $9
2065
+ elsif $13
1479
2066
  return Token.new(T_STAR, $+)
1480
- elsif $10
2067
+ elsif $14
1481
2068
  return Token.new(T_LBRA, $+)
1482
- elsif $11
2069
+ elsif $15
1483
2070
  return Token.new(T_RBRA, $+)
1484
- elsif $12
2071
+ elsif $16
1485
2072
  len = $+.to_i
1486
2073
  val = @str[@pos, len]
1487
2074
  @pos += len
1488
2075
  return Token.new(T_LITERAL, val)
1489
- elsif $13
1490
- return Token.new(T_PLUS, $+)
1491
- elsif $14
2076
+ elsif $17
1492
2077
  return Token.new(T_PERCENT, $+)
1493
- elsif $15
2078
+ elsif $18
1494
2079
  return Token.new(T_CRLF, $+)
1495
- elsif $16
2080
+ elsif $19
1496
2081
  return Token.new(T_EOF, $+)
1497
2082
  else
1498
2083
  parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
@@ -1511,8 +2096,7 @@ module Net
1511
2096
  elsif $3
1512
2097
  return Token.new(T_NUMBER, $+)
1513
2098
  elsif $4
1514
- return Token.new(T_QUOTED,
1515
- $+.gsub(/\\(["\\])/n, "\\1"))
2099
+ return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
1516
2100
  elsif $5
1517
2101
  len = $+.to_i
1518
2102
  val = @str[@pos, len]
@@ -1529,63 +2113,11 @@ module Net
1529
2113
  @str.index(/\S*/n, @pos)
1530
2114
  parse_error("unknown token - %s", $&.dump)
1531
2115
  end
1532
- when EXPR_TEXT
1533
- if @str.index(TEXT_REGEXP, @pos)
1534
- @pos = $~.end(0)
1535
- if $1
1536
- return Token.new(T_TEXT, $+)
1537
- else
1538
- parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
1539
- end
1540
- else
1541
- @str.index(/\S*/n, @pos)
1542
- parse_error("unknown token - %s", $&.dump)
1543
- end
1544
- when EXPR_RTEXT
1545
- if @str.index(RTEXT_REGEXP, @pos)
1546
- @pos = $~.end(0)
1547
- if $1
1548
- return Token.new(T_LBRA, $+)
1549
- elsif $2
1550
- return Token.new(T_TEXT, $+)
1551
- else
1552
- parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
1553
- end
1554
- else
1555
- @str.index(/\S*/n, @pos)
1556
- parse_error("unknown token - %s", $&.dump)
1557
- end
1558
- when EXPR_CTEXT
1559
- if @str.index(CTEXT_REGEXP, @pos)
1560
- @pos = $~.end(0)
1561
- if $1
1562
- return Token.new(T_TEXT, $+)
1563
- else
1564
- parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
1565
- end
1566
- else
1567
- @str.index(/\S*/n, @pos) #/
1568
- parse_error("unknown token - %s", $&.dump)
1569
- end
1570
2116
  else
1571
2117
  parse_error("invalid @lex_state - %s", @lex_state.inspect)
1572
2118
  end
1573
2119
  end
1574
2120
 
1575
- def parse_error(fmt, *args)
1576
- if IMAP.debug
1577
- $stderr.printf("@str: %s\n", @str.dump)
1578
- $stderr.printf("@pos: %d\n", @pos)
1579
- $stderr.printf("@lex_state: %s\n", @lex_state)
1580
- if @token
1581
- $stderr.printf("@token.symbol: %s\n", @token.symbol)
1582
- $stderr.printf("@token.value: %s\n", @token.value.inspect)
1583
- end
1584
- end
1585
- raise ResponseParseError, format(fmt, *args)
1586
- end
1587
2121
  end
1588
-
1589
2122
  end
1590
-
1591
2123
  end