net-imap 0.4.1 → 0.4.4
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/lib/net/imap/errors.rb +20 -0
- data/lib/net/imap/response_data.rb +46 -6
- data/lib/net/imap/response_parser/parser_utils.rb +14 -4
- data/lib/net/imap/response_parser.rb +609 -368
- data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -2
- data/lib/net/imap/sasl/authenticators.rb +2 -2
- data/lib/net/imap/sasl/cram_md5_authenticator.rb +7 -3
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +20 -8
- data/lib/net/imap/sasl/external_authenticator.rb +26 -5
- data/lib/net/imap/sasl/login_authenticator.rb +7 -3
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +73 -38
- data/lib/net/imap/sasl/plain_authenticator.rb +19 -11
- data/lib/net/imap/sasl/scram_authenticator.rb +19 -10
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +34 -16
- data/lib/net/imap.rb +26 -28
- data/net-imap.gemspec +3 -2
- data/rakelib/benchmarks.rake +98 -0
- metadata +3 -6
- data/benchmarks/generate_parser_benchmarks +0 -52
- data/benchmarks/parser.yml +0 -578
- data/benchmarks/stringprep.yml +0 -65
- data/benchmarks/table-regexps.yml +0 -39
@@ -58,6 +58,21 @@ module Net
|
|
58
58
|
T_TEXT = :TEXT # any char except CRLF
|
59
59
|
T_EOF = :EOF # end of response string
|
60
60
|
|
61
|
+
module ResponseConditions
|
62
|
+
OK = "OK"
|
63
|
+
NO = "NO"
|
64
|
+
BAD = "BAD"
|
65
|
+
BYE = "BYE"
|
66
|
+
PREAUTH = "PREAUTH"
|
67
|
+
|
68
|
+
RESP_COND_STATES = [OK, NO, BAD ].freeze
|
69
|
+
RESP_DATA_CONDS = [OK, NO, BAD, BYE, ].freeze
|
70
|
+
AUTH_CONDS = [OK, PREAUTH].freeze
|
71
|
+
GREETING_CONDS = [OK, BYE, PREAUTH].freeze
|
72
|
+
RESP_CONDS = [OK, NO, BAD, BYE, PREAUTH].freeze
|
73
|
+
end
|
74
|
+
include ResponseConditions
|
75
|
+
|
61
76
|
module Patterns
|
62
77
|
|
63
78
|
module CharClassSubtraction
|
@@ -170,6 +185,54 @@ module Net
|
|
170
185
|
CODE_TEXT_CHAR = TEXT_CHAR - RESP_SPECIALS
|
171
186
|
CODE_TEXT = /#{CODE_TEXT_CHAR}+/n
|
172
187
|
|
188
|
+
# flag = "\Answered" / "\Flagged" / "\Deleted" /
|
189
|
+
# "\Seen" / "\Draft" / flag-keyword / flag-extension
|
190
|
+
# ; Does not include "\Recent"
|
191
|
+
# flag-extension = "\" atom
|
192
|
+
# ; Future expansion. Client implementations
|
193
|
+
# ; MUST accept flag-extension flags. Server
|
194
|
+
# ; implementations MUST NOT generate
|
195
|
+
# ; flag-extension flags except as defined by
|
196
|
+
# ; a future Standard or Standards Track
|
197
|
+
# ; revisions of this specification.
|
198
|
+
# flag-keyword = "$MDNSent" / "$Forwarded" / "$Junk" /
|
199
|
+
# "$NotJunk" / "$Phishing" / atom
|
200
|
+
# flag-perm = flag / "\*"
|
201
|
+
#
|
202
|
+
# Not checking for max one mbx-list-sflag in the parser.
|
203
|
+
# >>>
|
204
|
+
# mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
|
205
|
+
# "\Subscribed" / "\Remote" / flag-extension
|
206
|
+
# ; Other flags; multiple from this list are
|
207
|
+
# ; possible per LIST response, but each flag
|
208
|
+
# ; can only appear once per LIST response
|
209
|
+
# mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
|
210
|
+
# "\Unmarked"
|
211
|
+
# ; Selectability flags; only one per LIST response
|
212
|
+
# child-mbox-flag = "\HasChildren" / "\HasNoChildren"
|
213
|
+
# ; attributes for the CHILDREN return option, at most
|
214
|
+
# ; one possible per LIST response
|
215
|
+
FLAG = /\\?#{ATOM}/n
|
216
|
+
FLAG_EXTENSION = /\\#{ATOM}/n
|
217
|
+
FLAG_KEYWORD = ATOM
|
218
|
+
FLAG_PERM = Regexp.union(FLAG, "\\*")
|
219
|
+
MBX_FLAG = FLAG_EXTENSION
|
220
|
+
|
221
|
+
# flag-list = "(" [flag *(SP flag)] ")"
|
222
|
+
#
|
223
|
+
# part of resp-text-code:
|
224
|
+
# >>>
|
225
|
+
# "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")"
|
226
|
+
#
|
227
|
+
# parens from mailbox-list are included in the regexp:
|
228
|
+
# >>>
|
229
|
+
# mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
|
230
|
+
# *(SP mbx-list-oflag) /
|
231
|
+
# mbx-list-oflag *(SP mbx-list-oflag)
|
232
|
+
FLAG_LIST = /\G\((#{FLAG }(?:#{SP}#{FLAG })*|)\)/ni
|
233
|
+
FLAG_PERM_LIST = /\G\((#{FLAG_PERM}(?:#{SP}#{FLAG_PERM})*|)\)/ni
|
234
|
+
MBX_LIST_FLAGS = /\G\((#{MBX_FLAG }(?:#{SP}#{MBX_FLAG })*|)\)/ni
|
235
|
+
|
173
236
|
# RFC3501:
|
174
237
|
# QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> /
|
175
238
|
# "\" quoted-specials
|
@@ -195,6 +258,14 @@ module Net
|
|
195
258
|
TEXT_rev1 = /#{TEXT_CHAR}+/
|
196
259
|
TEXT_rev2 = /#{Regexp.union TEXT_CHAR, UTF8_2, UTF8_3, UTF8_4}+/
|
197
260
|
|
261
|
+
# tagged-label-fchar = ALPHA / "-" / "_" / "."
|
262
|
+
TAGGED_LABEL_FCHAR = /[a-zA-Z\-_.]/n
|
263
|
+
# tagged-label-char = tagged-label-fchar / DIGIT / ":"
|
264
|
+
TAGGED_LABEL_CHAR = /[a-zA-Z\-_.0-9:]*/n
|
265
|
+
# tagged-ext-label = tagged-label-fchar *tagged-label-char
|
266
|
+
# ; Is a valid RFC 3501 "atom".
|
267
|
+
TAGGED_EXT_LABEL = /#{TAGGED_LABEL_FCHAR}#{TAGGED_LABEL_CHAR}*/n
|
268
|
+
|
198
269
|
# RFC3501:
|
199
270
|
# literal = "{" number "}" CRLF *CHAR8
|
200
271
|
# ; Number represents the number of CHAR8s
|
@@ -268,6 +339,8 @@ module Net
|
|
268
339
|
Token = Struct.new(:symbol, :value)
|
269
340
|
|
270
341
|
def_char_matchers :SP, " ", :T_SPACE
|
342
|
+
def_char_matchers :PLUS, "+", :T_PLUS
|
343
|
+
def_char_matchers :STAR, "*", :T_STAR
|
271
344
|
|
272
345
|
def_char_matchers :lpar, "(", :T_LPAR
|
273
346
|
def_char_matchers :rpar, ")", :T_RPAR
|
@@ -310,6 +383,9 @@ module Net
|
|
310
383
|
# TODO: add to lexer and only match tagged-ext-label
|
311
384
|
def_token_matchers :tagged_ext_label, T_ATOM, T_NIL, send: :upcase
|
312
385
|
|
386
|
+
def_token_matchers :CRLF, T_CRLF
|
387
|
+
def_token_matchers :EOF, T_EOF
|
388
|
+
|
313
389
|
# atom = 1*ATOM-CHAR
|
314
390
|
# ATOM-CHAR = <any CHAR except atom-specials>
|
315
391
|
ATOM_TOKENS = [T_ATOM, T_NUMBER, T_NIL, T_LBRA, T_PLUS]
|
@@ -320,10 +396,13 @@ module Net
|
|
320
396
|
|
321
397
|
ASTRING_TOKENS = [T_QUOTED, *ASTRING_CHARS_TOKENS, T_LITERAL].freeze
|
322
398
|
|
323
|
-
#
|
324
|
-
|
325
|
-
|
326
|
-
|
399
|
+
# tag = 1*<any ASTRING-CHAR except "+">
|
400
|
+
TAG_TOKENS = (ASTRING_CHARS_TOKENS - [T_PLUS]).freeze
|
401
|
+
|
402
|
+
# TODO: handle atom, astring_chars, and tag entirely inside the lexer
|
403
|
+
def atom; combine_adjacent(*ATOM_TOKENS) end
|
404
|
+
def astring_chars; combine_adjacent(*ASTRING_CHARS_TOKENS) end
|
405
|
+
def tag; combine_adjacent(*TAG_TOKENS) end
|
327
406
|
|
328
407
|
# the #accept version of #atom
|
329
408
|
def atom?; -combine_adjacent(*ATOM_TOKENS) if lookahead?(*ATOM_TOKENS) end
|
@@ -336,11 +415,6 @@ module Net
|
|
336
415
|
-combine_adjacent(*ATOM_TOKENS).upcase if lookahead?(*ATOM_TOKENS)
|
337
416
|
end
|
338
417
|
|
339
|
-
# TODO: handle astring_chars entirely inside the lexer
|
340
|
-
def astring_chars
|
341
|
-
combine_adjacent(*ASTRING_CHARS_TOKENS)
|
342
|
-
end
|
343
|
-
|
344
418
|
# astring = 1*ASTRING-CHAR / string
|
345
419
|
def astring
|
346
420
|
lookahead?(*ASTRING_CHARS_TOKENS) ? astring_chars : string
|
@@ -357,6 +431,30 @@ module Net
|
|
357
431
|
parse_error("unexpected atom %p, expected %p instead", val, word)
|
358
432
|
end
|
359
433
|
|
434
|
+
# Use #label or #label_in to assert specific known labels
|
435
|
+
# (+tagged-ext-label+ only, not +atom+).
|
436
|
+
def label_in(*labels)
|
437
|
+
lbl = tagged_ext_label and labels.include?(lbl) and return lbl
|
438
|
+
parse_error("unexpected atom %p, expected one of %s instead",
|
439
|
+
lbl, labels.join(" or "))
|
440
|
+
end
|
441
|
+
|
442
|
+
# expects "OK" or "PREAUTH" and raises InvalidResponseError on failure
|
443
|
+
def resp_cond_auth__name
|
444
|
+
lbl = tagged_ext_label and AUTH_CONDS.include? lbl and return lbl
|
445
|
+
raise InvalidResponseError, "bad response type %p, expected %s" % [
|
446
|
+
lbl, AUTH_CONDS.join(" or ")
|
447
|
+
]
|
448
|
+
end
|
449
|
+
|
450
|
+
# expects "OK" or "NO" or "BAD" and raises InvalidResponseError on failure
|
451
|
+
def resp_cond_state__name
|
452
|
+
lbl = tagged_ext_label and RESP_COND_STATES.include? lbl and return lbl
|
453
|
+
raise InvalidResponseError, "bad response type %p, expected %s" % [
|
454
|
+
lbl, RESP_COND_STATES.join(" or ")
|
455
|
+
]
|
456
|
+
end
|
457
|
+
|
360
458
|
# nstring = string / nil
|
361
459
|
def nstring
|
362
460
|
NIL? ? nil : string
|
@@ -378,155 +476,295 @@ module Net
|
|
378
476
|
alias number64 number
|
379
477
|
alias number64? number?
|
380
478
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
shift_token
|
394
|
-
end
|
395
|
-
match(T_CRLF)
|
396
|
-
match(T_EOF)
|
397
|
-
return result
|
398
|
-
end
|
479
|
+
# valid number ranges are not enforced by parser
|
480
|
+
# nz-number = digit-nz *DIGIT
|
481
|
+
# ; Non-zero unsigned 32-bit integer
|
482
|
+
# ; (0 < n < 4,294,967,296)
|
483
|
+
alias nz_number number
|
484
|
+
alias nz_number? number?
|
485
|
+
|
486
|
+
# valid number ranges are not enforced by parser
|
487
|
+
# nz-number64 = digit-nz *DIGIT
|
488
|
+
# ; Unsigned 63-bit integer
|
489
|
+
# ; (0 < n <= 9,223,372,036,854,775,807)
|
490
|
+
alias nz_number64 nz_number
|
399
491
|
|
492
|
+
# valid number ranges are not enforced by parser
|
493
|
+
# uniqueid = nz-number
|
494
|
+
# ; Strictly ascending
|
495
|
+
alias uniqueid nz_number
|
496
|
+
|
497
|
+
# [RFC3501 & RFC9051:]
|
498
|
+
# response = *(continue-req / response-data) response-done
|
499
|
+
#
|
500
|
+
# For simplicity, response isn't interpreted as the combination of the
|
501
|
+
# three response types, but instead represents any individual server
|
502
|
+
# response. Our simplified interpretation is defined as:
|
503
|
+
# response = continue-req | response_data | response-tagged
|
504
|
+
#
|
505
|
+
# n.b: our "response-tagged" definition parses "greeting" too.
|
506
|
+
def response
|
507
|
+
resp = case lookahead!(T_PLUS, T_STAR, *TAG_TOKENS).symbol
|
508
|
+
when T_PLUS then continue_req
|
509
|
+
when T_STAR then response_data
|
510
|
+
else response_tagged
|
511
|
+
end
|
512
|
+
accept_spaces # QUIRKY: Ignore trailing space (MS Exchange Server?)
|
513
|
+
CRLF!
|
514
|
+
EOF!
|
515
|
+
resp
|
516
|
+
end
|
517
|
+
|
518
|
+
# RFC3501 & RFC9051:
|
519
|
+
# continue-req = "+" SP (resp-text / base64) CRLF
|
520
|
+
#
|
521
|
+
# n.b: base64 is valid resp-text. And in the spirit of RFC9051 Appx E 23
|
522
|
+
# (and to workaround existing servers), we use the following grammar:
|
523
|
+
#
|
524
|
+
# continue-req = "+" (SP (resp-text)) CRLF
|
400
525
|
def continue_req
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
526
|
+
PLUS!
|
527
|
+
ContinuationRequest.new(SP? ? resp_text : ResponseText::EMPTY, @str)
|
528
|
+
end
|
529
|
+
|
530
|
+
RE_RESPONSE_TYPE = /\G(?:\d+ )?(?<type>#{Patterns::TAGGED_EXT_LABEL})/n
|
531
|
+
|
532
|
+
# [RFC3501:]
|
533
|
+
# response-data = "*" SP (resp-cond-state / resp-cond-bye /
|
534
|
+
# mailbox-data / message-data / capability-data) CRLF
|
535
|
+
# [RFC4466:]
|
536
|
+
# response-data = "*" SP response-payload CRLF
|
537
|
+
# response-payload = resp-cond-state / resp-cond-bye /
|
538
|
+
# mailbox-data / message-data / capability-data
|
539
|
+
# RFC5161 (ENABLE capability):
|
540
|
+
# response-data =/ "*" SP enable-data CRLF
|
541
|
+
# RFC5255 (LANGUAGE capability)
|
542
|
+
# response-payload =/ language-data
|
543
|
+
# RFC5255 (I18NLEVEL=1 and I18NLEVEL=2 capabilities)
|
544
|
+
# response-payload =/ comparator-data
|
545
|
+
# [RFC9051:]
|
546
|
+
# response-data = "*" SP (resp-cond-state / resp-cond-bye /
|
547
|
+
# mailbox-data / message-data / capability-data /
|
548
|
+
# enable-data) CRLF
|
549
|
+
#
|
550
|
+
# [merging in greeting and response-fatal:]
|
551
|
+
# greeting = "*" SP (resp-cond-auth / resp-cond-bye) CRLF
|
552
|
+
# response-fatal = "*" SP resp-cond-bye CRLF
|
553
|
+
# response-data =/ "*" SP (resp-cond-auth / resp-cond-bye) CRLF
|
554
|
+
# [removing duplicates, this is simply]
|
555
|
+
# response-payload =/ resp-cond-auth
|
556
|
+
#
|
557
|
+
# TODO: remove resp-cond-auth and handle greeting separately
|
558
|
+
def response_data
|
559
|
+
STAR!; SP!
|
560
|
+
m = peek_re(RE_RESPONSE_TYPE) or parse_error("unparsable response")
|
561
|
+
case m["type"].upcase
|
562
|
+
when "OK" then resp_cond_state__untagged # RFC3501, RFC9051
|
563
|
+
when "FETCH" then message_data__fetch # RFC3501, RFC9051
|
564
|
+
when "EXPUNGE" then message_data__expunge # RFC3501, RFC9051
|
565
|
+
when "EXISTS" then mailbox_data__exists # RFC3501, RFC9051
|
566
|
+
when "ESEARCH" then esearch_response # RFC4731, RFC9051, etc
|
567
|
+
when "VANISHED" then expunged_resp # RFC7162
|
568
|
+
when "UIDFETCH" then uidfetch_resp # (draft) UIDONLY
|
569
|
+
when "SEARCH" then mailbox_data__search # RFC3501 (obsolete)
|
570
|
+
when "CAPABILITY" then capability_data__untagged # RFC3501, RFC9051
|
571
|
+
when "FLAGS" then mailbox_data__flags # RFC3501, RFC9051
|
572
|
+
when "LIST" then mailbox_data__list # RFC3501, RFC9051
|
573
|
+
when "STATUS" then mailbox_data__status # RFC3501, RFC9051
|
574
|
+
when "NAMESPACE" then namespace_response # RFC2342, RFC9051
|
575
|
+
when "ENABLED" then enable_data # RFC5161, RFC9051
|
576
|
+
when "BAD" then resp_cond_state__untagged # RFC3501, RFC9051
|
577
|
+
when "NO" then resp_cond_state__untagged # RFC3501, RFC9051
|
578
|
+
when "PREAUTH" then resp_cond_auth # RFC3501, RFC9051
|
579
|
+
when "BYE" then resp_cond_bye # RFC3501, RFC9051
|
580
|
+
when "RECENT" then mailbox_data__recent # RFC3501 (obsolete)
|
581
|
+
when "SORT" then sort_data # RFC5256, RFC7162
|
582
|
+
when "THREAD" then thread_data # RFC5256
|
583
|
+
when "QUOTA" then quota_response # RFC2087, RFC9208
|
584
|
+
when "QUOTAROOT" then quotaroot_response # RFC2087, RFC9208
|
585
|
+
when "ID" then id_response # RFC2971
|
586
|
+
when "ACL" then acl_data # RFC4314
|
587
|
+
when "LISTRIGHTS" then listrights_data # RFC4314
|
588
|
+
when "MYRIGHTS" then myrights_data # RFC4314
|
589
|
+
when "METADATA" then metadata_resp # RFC5464
|
590
|
+
when "LANGUAGE" then language_data # RFC5255
|
591
|
+
when "COMPARATOR" then comparator_data # RFC5255
|
592
|
+
when "CONVERTED" then message_data__converted # RFC5259
|
593
|
+
when "LSUB" then mailbox_data__lsub # RFC3501 (obsolete)
|
594
|
+
when "XLIST" then mailbox_data__xlist # deprecated
|
595
|
+
when "NOOP" then response_data__noop
|
596
|
+
else response_data__unhandled
|
408
597
|
end
|
409
598
|
end
|
410
599
|
|
411
|
-
def
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
|
420
|
-
return response_cond
|
421
|
-
when /\A(?:FLAGS)\z/ni
|
422
|
-
return flags_response
|
423
|
-
when /\A(?:ID)\z/ni
|
424
|
-
return id_response
|
425
|
-
when /\A(?:LIST|LSUB|XLIST)\z/ni
|
426
|
-
return list_response
|
427
|
-
when /\A(?:NAMESPACE)\z/ni
|
428
|
-
return namespace_response
|
429
|
-
when /\A(?:QUOTA)\z/ni
|
430
|
-
return getquota_response
|
431
|
-
when /\A(?:QUOTAROOT)\z/ni
|
432
|
-
return getquotaroot_response
|
433
|
-
when /\A(?:ACL)\z/ni
|
434
|
-
return getacl_response
|
435
|
-
when /\A(?:SEARCH|SORT)\z/ni
|
436
|
-
return search_response
|
437
|
-
when /\A(?:THREAD)\z/ni
|
438
|
-
return thread_response
|
439
|
-
when /\A(?:STATUS)\z/ni
|
440
|
-
return status_response
|
441
|
-
when /\A(?:CAPABILITY)\z/ni
|
442
|
-
return capability_data__untagged
|
443
|
-
when /\A(?:NOOP)\z/ni
|
444
|
-
return ignored_response
|
445
|
-
when /\A(?:ENABLED)\z/ni
|
446
|
-
return enable_data
|
447
|
-
else
|
448
|
-
return text_response
|
600
|
+
def response_data__unhandled(klass = UntaggedResponse)
|
601
|
+
num = number?; SP?
|
602
|
+
type = tagged_ext_label; SP?
|
603
|
+
text = remaining_unparsed
|
604
|
+
data =
|
605
|
+
if num && text then UnparsedNumericResponseData.new(num, text)
|
606
|
+
elsif text then UnparsedData.new(text)
|
607
|
+
else num
|
449
608
|
end
|
450
|
-
|
451
|
-
|
452
|
-
|
609
|
+
klass.new(type, data, @str)
|
610
|
+
end
|
611
|
+
|
612
|
+
# reads all the way up until CRLF
|
613
|
+
def remaining_unparsed
|
614
|
+
str = @str[@pos...-2] and @pos += str.bytesize
|
615
|
+
str&.empty? ? nil : str
|
453
616
|
end
|
454
617
|
|
618
|
+
def response_data__ignored; response_data__unhandled(IgnoredResponse) end
|
619
|
+
alias response_data__noop response_data__ignored
|
620
|
+
|
621
|
+
alias esearch_response response_data__unhandled
|
622
|
+
alias expunged_resp response_data__unhandled
|
623
|
+
alias uidfetch_resp response_data__unhandled
|
624
|
+
alias listrights_data response_data__unhandled
|
625
|
+
alias myrights_data response_data__unhandled
|
626
|
+
alias metadata_resp response_data__unhandled
|
627
|
+
alias language_data response_data__unhandled
|
628
|
+
alias comparator_data response_data__unhandled
|
629
|
+
alias message_data__converted response_data__unhandled
|
630
|
+
|
631
|
+
# RFC3501 & RFC9051:
|
632
|
+
# response-tagged = tag SP resp-cond-state CRLF
|
633
|
+
#
|
634
|
+
# resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
|
635
|
+
# ; Status condition
|
636
|
+
#
|
637
|
+
# tag = 1*<any ASTRING-CHAR except "+">
|
455
638
|
def response_tagged
|
456
|
-
tag
|
457
|
-
|
458
|
-
|
459
|
-
name = token.value.upcase
|
460
|
-
match(T_SPACE)
|
461
|
-
return TaggedResponse.new(tag, name, resp_text, @str)
|
639
|
+
tag = tag(); SP!
|
640
|
+
name = resp_cond_state__name; SP!
|
641
|
+
TaggedResponse.new(tag, name, resp_text, @str)
|
462
642
|
end
|
463
643
|
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
644
|
+
# RFC3501 & RFC9051:
|
645
|
+
# resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
|
646
|
+
def resp_cond_state__untagged
|
647
|
+
name = resp_cond_state__name; SP!
|
648
|
+
UntaggedResponse.new(name, resp_text, @str)
|
469
649
|
end
|
470
650
|
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
name = token.value.upcase
|
476
|
-
case name
|
477
|
-
when "EXISTS", "RECENT", "EXPUNGE"
|
478
|
-
return UntaggedResponse.new(name, n, @str)
|
479
|
-
when "FETCH"
|
480
|
-
shift_token
|
481
|
-
match(T_SPACE)
|
482
|
-
data = FetchData.new(n, msg_att(n))
|
483
|
-
return UntaggedResponse.new(name, data, @str)
|
484
|
-
end
|
651
|
+
# resp-cond-auth = ("OK" / "PREAUTH") SP resp-text
|
652
|
+
def resp_cond_auth
|
653
|
+
name = resp_cond_auth__name; SP!
|
654
|
+
UntaggedResponse.new(name, resp_text, @str)
|
485
655
|
end
|
486
656
|
|
657
|
+
# resp-cond-bye = "BYE" SP resp-text
|
658
|
+
def resp_cond_bye
|
659
|
+
name = label(BYE); SP!
|
660
|
+
UntaggedResponse.new(name, resp_text, @str)
|
661
|
+
end
|
662
|
+
|
663
|
+
# message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
|
664
|
+
def message_data__fetch
|
665
|
+
seq = nz_number; SP!
|
666
|
+
name = label "FETCH"; SP!
|
667
|
+
data = FetchData.new(seq, msg_att(seq))
|
668
|
+
UntaggedResponse.new(name, data, @str)
|
669
|
+
end
|
670
|
+
|
671
|
+
def response_data__simple_numeric
|
672
|
+
data = nz_number; SP!
|
673
|
+
name = tagged_ext_label
|
674
|
+
UntaggedResponse.new(name, data, @str)
|
675
|
+
end
|
676
|
+
|
677
|
+
alias message_data__expunge response_data__simple_numeric
|
678
|
+
alias mailbox_data__exists response_data__simple_numeric
|
679
|
+
alias mailbox_data__recent response_data__simple_numeric
|
680
|
+
|
681
|
+
# RFC3501 & RFC9051:
|
682
|
+
# msg-att = "(" (msg-att-dynamic / msg-att-static)
|
683
|
+
# *(SP (msg-att-dynamic / msg-att-static)) ")"
|
684
|
+
#
|
685
|
+
# msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"
|
686
|
+
# RFC5257 (ANNOTATE extension):
|
687
|
+
# msg-att-dynamic =/ "ANNOTATION" SP
|
688
|
+
# ( "(" entry-att *(SP entry-att) ")" /
|
689
|
+
# "(" entry *(SP entry) ")" )
|
690
|
+
# RFC7162 (CONDSTORE extension):
|
691
|
+
# msg-att-dynamic =/ fetch-mod-resp
|
692
|
+
# fetch-mod-resp = "MODSEQ" SP "(" permsg-modsequence ")"
|
693
|
+
# RFC8970 (PREVIEW extension):
|
694
|
+
# msg-att-dynamic =/ "PREVIEW" SP nstring
|
695
|
+
#
|
696
|
+
# RFC3501:
|
697
|
+
# msg-att-static = "ENVELOPE" SP envelope /
|
698
|
+
# "INTERNALDATE" SP date-time /
|
699
|
+
# "RFC822" [".HEADER" / ".TEXT"] SP nstring /
|
700
|
+
# "RFC822.SIZE" SP number /
|
701
|
+
# "BODY" ["STRUCTURE"] SP body /
|
702
|
+
# "BODY" section ["<" number ">"] SP nstring /
|
703
|
+
# "UID" SP uniqueid
|
704
|
+
# RFC3516 (BINARY extension):
|
705
|
+
# msg-att-static =/ "BINARY" section-binary SP (nstring / literal8)
|
706
|
+
# / "BINARY.SIZE" section-binary SP number
|
707
|
+
# RFC8514 (SAVEDATE extension):
|
708
|
+
# msg-att-static =/ "SAVEDATE" SP (date-time / nil)
|
709
|
+
# RFC8474 (OBJECTID extension):
|
710
|
+
# msg-att-static =/ fetch-emailid-resp / fetch-threadid-resp
|
711
|
+
# fetch-emailid-resp = "EMAILID" SP "(" objectid ")"
|
712
|
+
# fetch-threadid-resp = "THREADID" SP ( "(" objectid ")" / nil )
|
713
|
+
# RFC9051:
|
714
|
+
# msg-att-static = "ENVELOPE" SP envelope /
|
715
|
+
# "INTERNALDATE" SP date-time /
|
716
|
+
# "RFC822.SIZE" SP number64 /
|
717
|
+
# "BODY" ["STRUCTURE"] SP body /
|
718
|
+
# "BODY" section ["<" number ">"] SP nstring /
|
719
|
+
# "BINARY" section-binary SP (nstring / literal8) /
|
720
|
+
# "BINARY.SIZE" section-binary SP number /
|
721
|
+
# "UID" SP uniqueid
|
722
|
+
#
|
723
|
+
# Re https://www.rfc-editor.org/errata/eid7246, I'm adding "offset" to the
|
724
|
+
# official "BINARY" ABNF, like so:
|
725
|
+
#
|
726
|
+
# msg-att-static =/ "BINARY" section-binary ["<" number ">"] SP
|
727
|
+
# (nstring / literal8)
|
487
728
|
def msg_att(n)
|
488
|
-
|
729
|
+
lpar
|
489
730
|
attr = {}
|
490
731
|
while true
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
name,
|
507
|
-
|
508
|
-
name, val = rfc822_text
|
509
|
-
when /\A(?:RFC822\.SIZE)\z/ni
|
510
|
-
name, val = rfc822_size
|
511
|
-
when /\A(?:BODY(?:STRUCTURE)?)\z/ni
|
512
|
-
name, val = body_data
|
513
|
-
when /\A(?:UID)\z/ni
|
514
|
-
name, val = uid_data
|
515
|
-
when /\A(?:MODSEQ)\z/ni
|
516
|
-
name, val = modseq_data
|
517
|
-
else
|
518
|
-
parse_error("unknown attribute `%s' for {%d}", token.value, n)
|
519
|
-
end
|
732
|
+
name = msg_att__label; SP!
|
733
|
+
val =
|
734
|
+
case name
|
735
|
+
when "UID" then uniqueid
|
736
|
+
when "FLAGS" then flag_list
|
737
|
+
when "BODY" then body
|
738
|
+
when /\ABODY\[/ni then nstring
|
739
|
+
when "BODYSTRUCTURE" then body
|
740
|
+
when "ENVELOPE" then envelope
|
741
|
+
when "INTERNALDATE" then date_time
|
742
|
+
when "RFC822.SIZE" then number64
|
743
|
+
when "RFC822" then nstring # not in rev2
|
744
|
+
when "RFC822.HEADER" then nstring # not in rev2
|
745
|
+
when "RFC822.TEXT" then nstring # not in rev2
|
746
|
+
when "MODSEQ" then parens__modseq # CONDSTORE
|
747
|
+
else parse_error("unknown attribute `%s' for {%d}", name, n)
|
748
|
+
end
|
520
749
|
attr[name] = val
|
750
|
+
break unless SP?
|
751
|
+
break if lookahead_rpar?
|
521
752
|
end
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
753
|
+
rpar
|
754
|
+
attr
|
755
|
+
end
|
756
|
+
|
757
|
+
# appends "[section]" and "<partial>" to the base label
|
758
|
+
def msg_att__label
|
759
|
+
case (name = tagged_ext_label)
|
760
|
+
when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
|
761
|
+
# ignoring "[]" fixes https://bugs.ruby-lang.org/issues/5620
|
762
|
+
lbra? and rbra
|
763
|
+
when "BODY"
|
764
|
+
peek_lbra? and name << section and
|
765
|
+
peek_str?("<") and name << atom # partial
|
766
|
+
end
|
767
|
+
name
|
530
768
|
end
|
531
769
|
|
532
770
|
def envelope
|
@@ -564,58 +802,10 @@ module Net
|
|
564
802
|
return result
|
565
803
|
end
|
566
804
|
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
return name, flag_list
|
572
|
-
end
|
573
|
-
|
574
|
-
def internaldate_data
|
575
|
-
token = match(T_ATOM)
|
576
|
-
name = token.value.upcase
|
577
|
-
match(T_SPACE)
|
578
|
-
token = match(T_QUOTED)
|
579
|
-
return name, token.value
|
580
|
-
end
|
581
|
-
|
582
|
-
def rfc822_text
|
583
|
-
token = match(T_ATOM)
|
584
|
-
name = token.value.upcase
|
585
|
-
token = lookahead
|
586
|
-
if token.symbol == T_LBRA
|
587
|
-
shift_token
|
588
|
-
match(T_RBRA)
|
589
|
-
end
|
590
|
-
match(T_SPACE)
|
591
|
-
return name, nstring
|
592
|
-
end
|
593
|
-
|
594
|
-
def rfc822_size
|
595
|
-
token = match(T_ATOM)
|
596
|
-
name = token.value.upcase
|
597
|
-
match(T_SPACE)
|
598
|
-
return name, number
|
599
|
-
end
|
600
|
-
|
601
|
-
def body_data
|
602
|
-
token = match(T_ATOM)
|
603
|
-
name = token.value.upcase
|
604
|
-
token = lookahead
|
605
|
-
if token.symbol == T_SPACE
|
606
|
-
shift_token
|
607
|
-
return name, body
|
608
|
-
end
|
609
|
-
name.concat(section)
|
610
|
-
token = lookahead
|
611
|
-
if token.symbol == T_ATOM
|
612
|
-
name.concat(token.value)
|
613
|
-
shift_token
|
614
|
-
end
|
615
|
-
match(T_SPACE)
|
616
|
-
data = nstring
|
617
|
-
return name, data
|
618
|
-
end
|
805
|
+
# date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
|
806
|
+
# SP time SP zone DQUOTE
|
807
|
+
alias date_time quoted
|
808
|
+
alias ndatetime nquoted
|
619
809
|
|
620
810
|
# RFC-3501 & RFC-9051:
|
621
811
|
# body = "(" (body-type-1part / body-type-mpart) ")"
|
@@ -844,6 +1034,7 @@ module Net
|
|
844
1034
|
if lpar?
|
845
1035
|
result = [case_insensitive__string]
|
846
1036
|
result << case_insensitive__string while SP?
|
1037
|
+
rpar
|
847
1038
|
result
|
848
1039
|
else
|
849
1040
|
case_insensitive__nstring
|
@@ -872,101 +1063,90 @@ module Net
|
|
872
1063
|
end
|
873
1064
|
end
|
874
1065
|
|
1066
|
+
# section = "[" [section-spec] "]"
|
875
1067
|
def section
|
876
|
-
str =
|
877
|
-
|
878
|
-
str
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
end
|
902
|
-
str.concat(format_string(astring))
|
903
|
-
end
|
904
|
-
end
|
905
|
-
token = match(T_RBRA)
|
906
|
-
str.concat(token.value)
|
907
|
-
return str
|
908
|
-
end
|
909
|
-
|
910
|
-
def format_string(str)
|
911
|
-
case str
|
912
|
-
when ""
|
913
|
-
return '""'
|
914
|
-
when /[\x80-\xff\r\n]/n
|
915
|
-
# literal
|
916
|
-
return "{" + str.bytesize.to_s + "}" + CRLF + str
|
917
|
-
when /[(){ \x00-\x1f\x7f%*"\\]/n
|
918
|
-
# quoted string
|
919
|
-
return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
|
920
|
-
else
|
921
|
-
# atom
|
922
|
-
return str
|
923
|
-
end
|
924
|
-
end
|
925
|
-
|
926
|
-
def uid_data
|
927
|
-
token = match(T_ATOM)
|
928
|
-
name = token.value.upcase
|
929
|
-
match(T_SPACE)
|
930
|
-
return name, number
|
931
|
-
end
|
932
|
-
|
933
|
-
def modseq_data
|
934
|
-
token = match(T_ATOM)
|
935
|
-
name = token.value.upcase
|
936
|
-
match(T_SPACE)
|
937
|
-
match(T_LPAR)
|
938
|
-
modseq = number
|
939
|
-
match(T_RPAR)
|
940
|
-
return name, modseq
|
941
|
-
end
|
942
|
-
|
943
|
-
def ignored_response
|
944
|
-
while lookahead.symbol != T_CRLF
|
945
|
-
shift_token
|
946
|
-
end
|
947
|
-
return IgnoredResponse.new(@str)
|
1068
|
+
str = +lbra
|
1069
|
+
str << section_spec unless peek_rbra?
|
1070
|
+
str << rbra
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
# section-spec = section-msgtext / (section-part ["." section-text])
|
1074
|
+
# section-msgtext = "HEADER" /
|
1075
|
+
# "HEADER.FIELDS" [".NOT"] SP header-list /
|
1076
|
+
# "TEXT"
|
1077
|
+
# ; top-level or MESSAGE/RFC822 or
|
1078
|
+
# ; MESSAGE/GLOBAL part
|
1079
|
+
# section-part = nz-number *("." nz-number)
|
1080
|
+
# ; body part reference.
|
1081
|
+
# ; Allows for accessing nested body parts.
|
1082
|
+
# section-text = section-msgtext / "MIME"
|
1083
|
+
# ; text other than actual body part (headers,
|
1084
|
+
# ; etc.)
|
1085
|
+
#
|
1086
|
+
# n.b: we could "cheat" here and just grab all text inside the brackets,
|
1087
|
+
# but literals would need special treatment.
|
1088
|
+
def section_spec
|
1089
|
+
str = "".b
|
1090
|
+
str << atom # grabs everything up to "SP header-list" or "]"
|
1091
|
+
str << " " << header_list if SP?
|
1092
|
+
str
|
948
1093
|
end
|
949
1094
|
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
1095
|
+
# header-list = "(" header-fld-name *(SP header-fld-name) ")"
|
1096
|
+
def header_list
|
1097
|
+
str = +""
|
1098
|
+
str << lpar << header_fld_name
|
1099
|
+
str << " " << header_fld_name while SP?
|
1100
|
+
str << rpar
|
955
1101
|
end
|
956
1102
|
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
1103
|
+
# RFC3501 & RFC9051:
|
1104
|
+
# header-fld-name = astring
|
1105
|
+
#
|
1106
|
+
# NOTE: Previously, Net::IMAP recreated the raw original source string.
|
1107
|
+
# Now, it grabs the raw encoded value using @str and @pos. A future
|
1108
|
+
# version may simply return the decoded astring value. Although that is
|
1109
|
+
# technically incompatible, it should almost never make a difference: all
|
1110
|
+
# standard header field names are valid atoms:
|
1111
|
+
#
|
1112
|
+
# https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
1113
|
+
#
|
1114
|
+
# Although RFC3501 allows any astring, RFC5322-valid header names are one
|
1115
|
+
# or more of the printable US-ASCII characters, except SP and colon. So
|
1116
|
+
# empty string isn't valid, and literals aren't needed and should not be
|
1117
|
+
# used. This is explicitly unchanged by [I18N-HDRS] (RFC6532).
|
1118
|
+
#
|
1119
|
+
# RFC5233:
|
1120
|
+
# optional-field = field-name ":" unstructured CRLF
|
1121
|
+
# field-name = 1*ftext
|
1122
|
+
# ftext = %d33-57 / ; Printable US-ASCII
|
1123
|
+
# %d59-126 ; characters not including
|
1124
|
+
# ; ":".
|
1125
|
+
def header_fld_name
|
1126
|
+
assert_no_lookahead
|
1127
|
+
start = @pos
|
1128
|
+
astring
|
1129
|
+
@str[start...@pos - 1]
|
1130
|
+
end
|
1131
|
+
|
1132
|
+
# mailbox-data = "FLAGS" SP flag-list / "LIST" SP mailbox-list /
|
1133
|
+
# "LSUB" SP mailbox-list / "SEARCH" *(SP nz-number) /
|
1134
|
+
# "STATUS" SP mailbox SP "(" [status-att-list] ")" /
|
1135
|
+
# number SP "EXISTS" / number SP "RECENT"
|
1136
|
+
|
1137
|
+
def mailbox_data__flags
|
1138
|
+
name = label("FLAGS")
|
1139
|
+
SP!
|
1140
|
+
UntaggedResponse.new(name, flag_list, @str)
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
def mailbox_data__list
|
1144
|
+
name = label_in("LIST", "LSUB", "XLIST")
|
1145
|
+
SP!
|
1146
|
+
UntaggedResponse.new(name, mailbox_list, @str)
|
1147
|
+
end
|
1148
|
+
alias mailbox_data__lsub mailbox_data__list
|
1149
|
+
alias mailbox_data__xlist mailbox_data__list
|
970
1150
|
|
971
1151
|
def mailbox_list
|
972
1152
|
attr = flag_list
|
@@ -1032,7 +1212,8 @@ module Net
|
|
1032
1212
|
return UntaggedResponse.new(name, data, @str)
|
1033
1213
|
end
|
1034
1214
|
|
1035
|
-
|
1215
|
+
# acl-data = "ACL" SP mailbox *(SP identifier SP rights)
|
1216
|
+
def acl_data
|
1036
1217
|
token = match(T_ATOM)
|
1037
1218
|
name = token.value.upcase
|
1038
1219
|
match(T_SPACE)
|
@@ -1058,7 +1239,21 @@ module Net
|
|
1058
1239
|
return UntaggedResponse.new(name, data, @str)
|
1059
1240
|
end
|
1060
1241
|
|
1061
|
-
|
1242
|
+
# RFC3501:
|
1243
|
+
# mailbox-data = "SEARCH" *(SP nz-number) / ...
|
1244
|
+
# RFC5256: SORT
|
1245
|
+
# sort-data = "SORT" *(SP nz-number)
|
1246
|
+
# RFC7162: CONDSTORE, QRESYNC
|
1247
|
+
# mailbox-data =/ "SEARCH" [1*(SP nz-number) SP
|
1248
|
+
# search-sort-mod-seq]
|
1249
|
+
# sort-data = "SORT" [1*(SP nz-number) SP
|
1250
|
+
# search-sort-mod-seq]
|
1251
|
+
# ; Updates the SORT response from RFC 5256.
|
1252
|
+
# search-sort-mod-seq = "(" "MODSEQ" SP mod-sequence-value ")"
|
1253
|
+
# RFC9051:
|
1254
|
+
# mailbox-data = obsolete-search-response / ...
|
1255
|
+
# obsolete-search-response = "SEARCH" *(SP nz-number)
|
1256
|
+
def mailbox_data__search
|
1062
1257
|
token = match(T_ATOM)
|
1063
1258
|
name = token.value.upcase
|
1064
1259
|
token = lookahead
|
@@ -1088,8 +1283,9 @@ module Net
|
|
1088
1283
|
end
|
1089
1284
|
return UntaggedResponse.new(name, data, @str)
|
1090
1285
|
end
|
1286
|
+
alias sort_data mailbox_data__search
|
1091
1287
|
|
1092
|
-
def
|
1288
|
+
def thread_data
|
1093
1289
|
token = match(T_ATOM)
|
1094
1290
|
name = token.value.upcase
|
1095
1291
|
token = lookahead
|
@@ -1151,7 +1347,7 @@ module Net
|
|
1151
1347
|
return rootmember
|
1152
1348
|
end
|
1153
1349
|
|
1154
|
-
def
|
1350
|
+
def mailbox_data__status
|
1155
1351
|
token = match(T_ATOM)
|
1156
1352
|
name = token.value.upcase
|
1157
1353
|
match(T_SPACE)
|
@@ -1198,11 +1394,13 @@ module Net
|
|
1198
1394
|
end
|
1199
1395
|
|
1200
1396
|
# As a workaround for buggy servers, allow a trailing SP:
|
1201
|
-
# *(SP
|
1397
|
+
# *(SP capability) [SP]
|
1202
1398
|
def capability__list
|
1203
|
-
|
1399
|
+
list = []; while SP? && (capa = capability?) do list << capa end; list
|
1204
1400
|
end
|
1205
1401
|
|
1402
|
+
alias resp_code__capability capability__list
|
1403
|
+
|
1206
1404
|
# capability = ("AUTH=" auth-type) / atom
|
1207
1405
|
# ; New capabilities MUST begin with "X" or be
|
1208
1406
|
# ; registered with IANA as standard or
|
@@ -1325,68 +1523,91 @@ module Net
|
|
1325
1523
|
end
|
1326
1524
|
end
|
1327
1525
|
|
1328
|
-
# See https://www.rfc-editor.org/errata/rfc3501
|
1526
|
+
# RFC3501 (See https://www.rfc-editor.org/errata/rfc3501):
|
1527
|
+
# resp-text-code = "ALERT" /
|
1528
|
+
# "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
|
1529
|
+
# capability-data / "PARSE" /
|
1530
|
+
# "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
|
1531
|
+
# "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
|
1532
|
+
# "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
|
1533
|
+
# "UNSEEN" SP nz-number /
|
1534
|
+
# atom [SP 1*<any TEXT-CHAR except "]">]
|
1535
|
+
# capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1"
|
1536
|
+
# *(SP capability)
|
1329
1537
|
#
|
1330
|
-
#
|
1331
|
-
#
|
1332
|
-
#
|
1333
|
-
#
|
1334
|
-
#
|
1335
|
-
#
|
1336
|
-
#
|
1337
|
-
#
|
1338
|
-
#
|
1538
|
+
# RFC5530:
|
1539
|
+
# resp-text-code =/ "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
|
1540
|
+
# "AUTHORIZATIONFAILED" / "EXPIRED" /
|
1541
|
+
# "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
|
1542
|
+
# "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
|
1543
|
+
# "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
|
1544
|
+
# "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
|
1545
|
+
# "NONEXISTENT"
|
1546
|
+
# RFC9051:
|
1547
|
+
# resp-text-code = "ALERT" /
|
1548
|
+
# "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
|
1549
|
+
# capability-data / "PARSE" /
|
1550
|
+
# "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
|
1551
|
+
# "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
|
1552
|
+
# "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
|
1553
|
+
# resp-code-apnd / resp-code-copy / "UIDNOTSTICKY" /
|
1554
|
+
# "UNAVAILABLE" / "AUTHENTICATIONFAILED" /
|
1555
|
+
# "AUTHORIZATIONFAILED" / "EXPIRED" /
|
1556
|
+
# "PRIVACYREQUIRED" / "CONTACTADMIN" / "NOPERM" /
|
1557
|
+
# "INUSE" / "EXPUNGEISSUED" / "CORRUPTION" /
|
1558
|
+
# "SERVERBUG" / "CLIENTBUG" / "CANNOT" /
|
1559
|
+
# "LIMIT" / "OVERQUOTA" / "ALREADYEXISTS" /
|
1560
|
+
# "NONEXISTENT" / "NOTSAVED" / "HASCHILDREN" /
|
1561
|
+
# "CLOSED" /
|
1562
|
+
# "UNKNOWN-CTE" /
|
1563
|
+
# atom [SP 1*<any TEXT-CHAR except "]">]
|
1564
|
+
# capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev2"
|
1565
|
+
# *(SP capability)
|
1339
1566
|
#
|
1340
|
-
#
|
1341
|
-
# resp-
|
1567
|
+
# RFC4315 (UIDPLUS), RFC9051 (IMAP4rev2):
|
1568
|
+
# resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
|
1569
|
+
# resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
|
1570
|
+
# resp-text-code =/ resp-code-apnd / resp-code-copy / "UIDNOTSTICKY"
|
1571
|
+
#
|
1572
|
+
# RFC7162 (CONDSTORE):
|
1573
|
+
# resp-text-code =/ "HIGHESTMODSEQ" SP mod-sequence-value /
|
1574
|
+
# "NOMODSEQ" /
|
1575
|
+
# "MODIFIED" SP sequence-set
|
1342
1576
|
def resp_text_code
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
result = ResponseCode.new(name, resp_code_copy__data)
|
1362
|
-
else
|
1363
|
-
token = lookahead
|
1364
|
-
if token.symbol == T_SPACE
|
1365
|
-
shift_token
|
1366
|
-
result = ResponseCode.new(name, text_chars_except_rbra)
|
1577
|
+
name = resp_text_code__name
|
1578
|
+
data =
|
1579
|
+
case name
|
1580
|
+
when "CAPABILITY" then resp_code__capability
|
1581
|
+
when "PERMANENTFLAGS" then SP? ? flag_perm__list : []
|
1582
|
+
when "UIDNEXT" then SP!; nz_number
|
1583
|
+
when "UIDVALIDITY" then SP!; nz_number
|
1584
|
+
when "UNSEEN" then SP!; nz_number # rev1 only
|
1585
|
+
when "APPENDUID" then SP!; resp_code_apnd__data # rev2, UIDPLUS
|
1586
|
+
when "COPYUID" then SP!; resp_code_copy__data # rev2, UIDPLUS
|
1587
|
+
when "BADCHARSET" then SP? ? charset__list : []
|
1588
|
+
when "ALERT", "PARSE", "READ-ONLY", "READ-WRITE", "TRYCREATE",
|
1589
|
+
"UNAVAILABLE", "AUTHENTICATIONFAILED", "AUTHORIZATIONFAILED",
|
1590
|
+
"EXPIRED", "PRIVACYREQUIRED", "CONTACTADMIN", "NOPERM", "INUSE",
|
1591
|
+
"EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT",
|
1592
|
+
"LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "CLOSED",
|
1593
|
+
"NOTSAVED", "UIDNOTSTICKY", "UNKNOWN-CTE", "HASCHILDREN"
|
1594
|
+
when "NOMODSEQ" # CONDSTORE
|
1367
1595
|
else
|
1368
|
-
|
1596
|
+
SP? and text_chars_except_rbra
|
1369
1597
|
end
|
1370
|
-
|
1371
|
-
return result
|
1598
|
+
ResponseCode.new(name, data)
|
1372
1599
|
end
|
1373
1600
|
|
1601
|
+
alias resp_text_code__name case_insensitive__atom
|
1602
|
+
|
1374
1603
|
# 1*<any TEXT-CHAR except "]">
|
1375
1604
|
def text_chars_except_rbra
|
1376
1605
|
match_re(CTEXT_REGEXP, '1*<any TEXT-CHAR except "]">')[0]
|
1377
1606
|
end
|
1378
1607
|
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
match(T_LPAR)
|
1383
|
-
result << charset
|
1384
|
-
while accept(T_SPACE)
|
1385
|
-
result << charset
|
1386
|
-
end
|
1387
|
-
match(T_RPAR)
|
1388
|
-
end
|
1389
|
-
result
|
1608
|
+
# "(" charset *(SP charset) ")"
|
1609
|
+
def charset__list
|
1610
|
+
lpar; list = [charset]; while SP? do list << charset end; rpar; list
|
1390
1611
|
end
|
1391
1612
|
|
1392
1613
|
# already matched: "APPENDUID"
|
@@ -1402,8 +1623,8 @@ module Net
|
|
1402
1623
|
# match uid_set even if that returns a single-member array.
|
1403
1624
|
#
|
1404
1625
|
def resp_code_apnd__data
|
1405
|
-
|
1406
|
-
|
1626
|
+
validity = number; SP!
|
1627
|
+
dst_uids = uid_set # uniqueid ⊂ uid-set
|
1407
1628
|
UIDPlusData.new(validity, nil, dst_uids)
|
1408
1629
|
end
|
1409
1630
|
|
@@ -1411,9 +1632,9 @@ module Net
|
|
1411
1632
|
#
|
1412
1633
|
# resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
|
1413
1634
|
def resp_code_copy__data
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1635
|
+
validity = number; SP!
|
1636
|
+
src_uids = uid_set; SP!
|
1637
|
+
dst_uids = uid_set
|
1417
1638
|
UIDPlusData.new(validity, src_uids, dst_uids)
|
1418
1639
|
end
|
1419
1640
|
|
@@ -1472,36 +1693,56 @@ module Net
|
|
1472
1693
|
return Address.new(name, route, mailbox, host)
|
1473
1694
|
end
|
1474
1695
|
|
1475
|
-
|
1476
|
-
(?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
|
1477
|
-
(?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
|
1478
|
-
|
1696
|
+
# flag-list = "(" [flag *(SP flag)] ")"
|
1479
1697
|
def flag_list
|
1480
|
-
|
1481
|
-
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1698
|
+
match_re(Patterns::FLAG_LIST, "flag-list")[1]
|
1699
|
+
.split(nil)
|
1700
|
+
.map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
|
1701
|
+
end
|
1702
|
+
|
1703
|
+
# "(" [flag-perm *(SP flag-perm)] ")"
|
1704
|
+
def flag_perm__list
|
1705
|
+
match_re(Patterns::FLAG_PERM_LIST, "PERMANENTFLAGS flag-perm list")[1]
|
1706
|
+
.split(nil)
|
1707
|
+
.map! { _1.start_with?("\\") ? _1[1..].capitalize.to_sym : _1 }
|
1708
|
+
end
|
1709
|
+
|
1710
|
+
# Not checking for max one mbx-list-sflag in the parser.
|
1711
|
+
# >>>
|
1712
|
+
# mbx-list-flags = *(mbx-list-oflag SP) mbx-list-sflag
|
1713
|
+
# *(SP mbx-list-oflag) /
|
1714
|
+
# mbx-list-oflag *(SP mbx-list-oflag)
|
1715
|
+
# mbx-list-oflag = "\Noinferiors" / child-mbox-flag /
|
1716
|
+
# "\Subscribed" / "\Remote" / flag-extension
|
1717
|
+
# ; Other flags; multiple from this list are
|
1718
|
+
# ; possible per LIST response, but each flag
|
1719
|
+
# ; can only appear once per LIST response
|
1720
|
+
# mbx-list-sflag = "\NonExistent" / "\Noselect" / "\Marked" /
|
1721
|
+
# "\Unmarked"
|
1722
|
+
# ; Selectability flags; only one per LIST response
|
1723
|
+
def parens__mbx_list_flags
|
1724
|
+
match_re(Patterns::MBX_LIST_FLAGS, "mbx-list-flags")[1]
|
1725
|
+
.split(nil).map! { _1.capitalize.to_sym }
|
1492
1726
|
end
|
1493
1727
|
|
1494
|
-
|
1495
1728
|
# See https://www.rfc-editor.org/errata/rfc3501
|
1496
1729
|
#
|
1497
1730
|
# charset = atom / quoted
|
1498
|
-
def charset
|
1499
|
-
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1731
|
+
def charset; quoted? || atom end
|
1732
|
+
|
1733
|
+
# RFC7162:
|
1734
|
+
# mod-sequence-value = 1*DIGIT
|
1735
|
+
# ;; Positive unsigned 63-bit integer
|
1736
|
+
# ;; (mod-sequence)
|
1737
|
+
# ;; (1 <= n <= 9,223,372,036,854,775,807).
|
1738
|
+
alias mod_sequence_value nz_number64
|
1739
|
+
|
1740
|
+
# RFC7162:
|
1741
|
+
# permsg-modsequence = mod-sequence-value
|
1742
|
+
# ;; Per-message mod-sequence.
|
1743
|
+
alias permsg_modsequence mod_sequence_value
|
1744
|
+
|
1745
|
+
def parens__modseq; lpar; _ = permsg_modsequence; rpar; _ end
|
1505
1746
|
|
1506
1747
|
# RFC-4315 (UIDPLUS) or RFC9051 (IMAP4rev2):
|
1507
1748
|
# uid-set = (uniqueid / uid-range) *("," uid-set)
|
@@ -1535,10 +1776,10 @@ module Net
|
|
1535
1776
|
#
|
1536
1777
|
# This advances @pos directly so it's safe before changing @lex_state.
|
1537
1778
|
def accept_spaces
|
1538
|
-
|
1539
|
-
|
1779
|
+
return false unless SP?
|
1780
|
+
@str.index(SPACES_REGEXP, @pos) and
|
1540
1781
|
@pos = $~.end(0)
|
1541
|
-
|
1782
|
+
true
|
1542
1783
|
end
|
1543
1784
|
|
1544
1785
|
def next_token
|