net-imap 0.1.1 → 0.2.3

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.

@@ -0,0 +1,1526 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Net
6
+ class IMAP < Protocol
7
+
8
+ class ResponseParser # :nodoc:
9
+ def initialize
10
+ @str = nil
11
+ @pos = nil
12
+ @lex_state = nil
13
+ @token = nil
14
+ end
15
+
16
+ def parse(str)
17
+ @str = str
18
+ @pos = 0
19
+ @lex_state = EXPR_BEG
20
+ @token = nil
21
+ return response
22
+ end
23
+
24
+ private
25
+
26
+ EXPR_BEG = :EXPR_BEG
27
+ EXPR_DATA = :EXPR_DATA
28
+ EXPR_TEXT = :EXPR_TEXT
29
+ EXPR_RTEXT = :EXPR_RTEXT
30
+ EXPR_CTEXT = :EXPR_CTEXT
31
+
32
+ T_SPACE = :SPACE
33
+ T_NIL = :NIL
34
+ T_NUMBER = :NUMBER
35
+ T_ATOM = :ATOM
36
+ T_QUOTED = :QUOTED
37
+ T_LPAR = :LPAR
38
+ T_RPAR = :RPAR
39
+ T_BSLASH = :BSLASH
40
+ T_STAR = :STAR
41
+ T_LBRA = :LBRA
42
+ T_RBRA = :RBRA
43
+ T_LITERAL = :LITERAL
44
+ T_PLUS = :PLUS
45
+ T_PERCENT = :PERCENT
46
+ T_CRLF = :CRLF
47
+ T_EOF = :EOF
48
+ T_TEXT = :TEXT
49
+
50
+ BEG_REGEXP = /\G(?:\
51
+ (?# 1: SPACE )( +)|\
52
+ (?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
53
+ (?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
54
+ (?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
55
+ (?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
56
+ (?# 6: LPAR )(\()|\
57
+ (?# 7: RPAR )(\))|\
58
+ (?# 8: BSLASH )(\\)|\
59
+ (?# 9: STAR )(\*)|\
60
+ (?# 10: LBRA )(\[)|\
61
+ (?# 11: RBRA )(\])|\
62
+ (?# 12: LITERAL )\{(\d+)\}\r\n|\
63
+ (?# 13: PLUS )(\+)|\
64
+ (?# 14: PERCENT )(%)|\
65
+ (?# 15: CRLF )(\r\n)|\
66
+ (?# 16: EOF )(\z))/ni
67
+
68
+ DATA_REGEXP = /\G(?:\
69
+ (?# 1: SPACE )( )|\
70
+ (?# 2: NIL )(NIL)|\
71
+ (?# 3: NUMBER )(\d+)|\
72
+ (?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
73
+ (?# 5: LITERAL )\{(\d+)\}\r\n|\
74
+ (?# 6: LPAR )(\()|\
75
+ (?# 7: RPAR )(\)))/ni
76
+
77
+ TEXT_REGEXP = /\G(?:\
78
+ (?# 1: TEXT )([^\x00\r\n]*))/ni
79
+
80
+ RTEXT_REGEXP = /\G(?:\
81
+ (?# 1: LBRA )(\[)|\
82
+ (?# 2: TEXT )([^\x00\r\n]*))/ni
83
+
84
+ CTEXT_REGEXP = /\G(?:\
85
+ (?# 1: TEXT )([^\x00\r\n\]]*))/ni
86
+
87
+ Token = Struct.new(:symbol, :value)
88
+
89
+ def response
90
+ token = lookahead
91
+ case token.symbol
92
+ when T_PLUS
93
+ result = continue_req
94
+ when T_STAR
95
+ result = response_untagged
96
+ else
97
+ result = response_tagged
98
+ end
99
+ while lookahead.symbol == T_SPACE
100
+ # Ignore trailing space for Microsoft Exchange Server
101
+ shift_token
102
+ end
103
+ match(T_CRLF)
104
+ match(T_EOF)
105
+ return result
106
+ end
107
+
108
+ def continue_req
109
+ match(T_PLUS)
110
+ token = lookahead
111
+ if token.symbol == T_SPACE
112
+ shift_token
113
+ return ContinuationRequest.new(resp_text, @str)
114
+ else
115
+ return ContinuationRequest.new(ResponseText.new(nil, ""), @str)
116
+ end
117
+ end
118
+
119
+ def response_untagged
120
+ match(T_STAR)
121
+ match(T_SPACE)
122
+ token = lookahead
123
+ if token.symbol == T_NUMBER
124
+ return numeric_response
125
+ elsif token.symbol == T_ATOM
126
+ case token.value
127
+ when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
128
+ return response_cond
129
+ when /\A(?:FLAGS)\z/ni
130
+ return flags_response
131
+ when /\A(?:ID)\z/ni
132
+ return id_response
133
+ when /\A(?:LIST|LSUB|XLIST)\z/ni
134
+ return list_response
135
+ when /\A(?:NAMESPACE)\z/ni
136
+ return namespace_response
137
+ when /\A(?:QUOTA)\z/ni
138
+ return getquota_response
139
+ when /\A(?:QUOTAROOT)\z/ni
140
+ return getquotaroot_response
141
+ when /\A(?:ACL)\z/ni
142
+ return getacl_response
143
+ when /\A(?:SEARCH|SORT)\z/ni
144
+ return search_response
145
+ when /\A(?:THREAD)\z/ni
146
+ return thread_response
147
+ when /\A(?:STATUS)\z/ni
148
+ return status_response
149
+ when /\A(?:CAPABILITY)\z/ni
150
+ return capability_response
151
+ when /\A(?:NOOP)\z/ni
152
+ return ignored_response
153
+ else
154
+ return text_response
155
+ end
156
+ else
157
+ parse_error("unexpected token %s", token.symbol)
158
+ end
159
+ end
160
+
161
+ def response_tagged
162
+ tag = astring_chars
163
+ match(T_SPACE)
164
+ token = match(T_ATOM)
165
+ name = token.value.upcase
166
+ match(T_SPACE)
167
+ return TaggedResponse.new(tag, name, resp_text, @str)
168
+ end
169
+
170
+ def response_cond
171
+ token = match(T_ATOM)
172
+ name = token.value.upcase
173
+ match(T_SPACE)
174
+ return UntaggedResponse.new(name, resp_text, @str)
175
+ end
176
+
177
+ def numeric_response
178
+ n = number
179
+ match(T_SPACE)
180
+ token = match(T_ATOM)
181
+ name = token.value.upcase
182
+ case name
183
+ when "EXISTS", "RECENT", "EXPUNGE"
184
+ return UntaggedResponse.new(name, n, @str)
185
+ when "FETCH"
186
+ shift_token
187
+ match(T_SPACE)
188
+ data = FetchData.new(n, msg_att(n))
189
+ return UntaggedResponse.new(name, data, @str)
190
+ end
191
+ end
192
+
193
+ def msg_att(n)
194
+ match(T_LPAR)
195
+ attr = {}
196
+ while true
197
+ token = lookahead
198
+ case token.symbol
199
+ when T_RPAR
200
+ shift_token
201
+ break
202
+ when T_SPACE
203
+ shift_token
204
+ next
205
+ end
206
+ case token.value
207
+ when /\A(?:ENVELOPE)\z/ni
208
+ name, val = envelope_data
209
+ when /\A(?:FLAGS)\z/ni
210
+ name, val = flags_data
211
+ when /\A(?:INTERNALDATE)\z/ni
212
+ name, val = internaldate_data
213
+ when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
214
+ name, val = rfc822_text
215
+ when /\A(?:RFC822\.SIZE)\z/ni
216
+ name, val = rfc822_size
217
+ when /\A(?:BODY(?:STRUCTURE)?)\z/ni
218
+ name, val = body_data
219
+ when /\A(?:UID)\z/ni
220
+ name, val = uid_data
221
+ when /\A(?:MODSEQ)\z/ni
222
+ name, val = modseq_data
223
+ else
224
+ parse_error("unknown attribute `%s' for {%d}", token.value, n)
225
+ end
226
+ attr[name] = val
227
+ end
228
+ return attr
229
+ end
230
+
231
+ def envelope_data
232
+ token = match(T_ATOM)
233
+ name = token.value.upcase
234
+ match(T_SPACE)
235
+ return name, envelope
236
+ end
237
+
238
+ def envelope
239
+ @lex_state = EXPR_DATA
240
+ token = lookahead
241
+ if token.symbol == T_NIL
242
+ shift_token
243
+ result = nil
244
+ else
245
+ match(T_LPAR)
246
+ date = nstring
247
+ match(T_SPACE)
248
+ subject = nstring
249
+ match(T_SPACE)
250
+ from = address_list
251
+ match(T_SPACE)
252
+ sender = address_list
253
+ match(T_SPACE)
254
+ reply_to = address_list
255
+ match(T_SPACE)
256
+ to = address_list
257
+ match(T_SPACE)
258
+ cc = address_list
259
+ match(T_SPACE)
260
+ bcc = address_list
261
+ match(T_SPACE)
262
+ in_reply_to = nstring
263
+ match(T_SPACE)
264
+ message_id = nstring
265
+ match(T_RPAR)
266
+ result = Envelope.new(date, subject, from, sender, reply_to,
267
+ to, cc, bcc, in_reply_to, message_id)
268
+ end
269
+ @lex_state = EXPR_BEG
270
+ return result
271
+ end
272
+
273
+ def flags_data
274
+ token = match(T_ATOM)
275
+ name = token.value.upcase
276
+ match(T_SPACE)
277
+ return name, flag_list
278
+ end
279
+
280
+ def internaldate_data
281
+ token = match(T_ATOM)
282
+ name = token.value.upcase
283
+ match(T_SPACE)
284
+ token = match(T_QUOTED)
285
+ return name, token.value
286
+ end
287
+
288
+ def rfc822_text
289
+ token = match(T_ATOM)
290
+ name = token.value.upcase
291
+ token = lookahead
292
+ if token.symbol == T_LBRA
293
+ shift_token
294
+ match(T_RBRA)
295
+ end
296
+ match(T_SPACE)
297
+ return name, nstring
298
+ end
299
+
300
+ def rfc822_size
301
+ token = match(T_ATOM)
302
+ name = token.value.upcase
303
+ match(T_SPACE)
304
+ return name, number
305
+ end
306
+
307
+ def body_data
308
+ token = match(T_ATOM)
309
+ name = token.value.upcase
310
+ token = lookahead
311
+ if token.symbol == T_SPACE
312
+ shift_token
313
+ return name, body
314
+ end
315
+ name.concat(section)
316
+ token = lookahead
317
+ if token.symbol == T_ATOM
318
+ name.concat(token.value)
319
+ shift_token
320
+ end
321
+ match(T_SPACE)
322
+ data = nstring
323
+ return name, data
324
+ end
325
+
326
+ def body
327
+ @lex_state = EXPR_DATA
328
+ token = lookahead
329
+ if token.symbol == T_NIL
330
+ shift_token
331
+ result = nil
332
+ else
333
+ match(T_LPAR)
334
+ token = lookahead
335
+ if token.symbol == T_LPAR
336
+ result = body_type_mpart
337
+ else
338
+ result = body_type_1part
339
+ end
340
+ match(T_RPAR)
341
+ end
342
+ @lex_state = EXPR_BEG
343
+ return result
344
+ end
345
+
346
+ def body_type_1part
347
+ token = lookahead
348
+ case token.value
349
+ when /\A(?:TEXT)\z/ni
350
+ return body_type_text
351
+ when /\A(?:MESSAGE)\z/ni
352
+ return body_type_msg
353
+ when /\A(?:ATTACHMENT)\z/ni
354
+ return body_type_attachment
355
+ when /\A(?:MIXED)\z/ni
356
+ return body_type_mixed
357
+ else
358
+ return body_type_basic
359
+ end
360
+ end
361
+
362
+ def body_type_basic
363
+ mtype, msubtype = media_type
364
+ token = lookahead
365
+ if token.symbol == T_RPAR
366
+ return BodyTypeBasic.new(mtype, msubtype)
367
+ end
368
+ match(T_SPACE)
369
+ param, content_id, desc, enc, size = body_fields
370
+ md5, disposition, language, extension = body_ext_1part
371
+ return BodyTypeBasic.new(mtype, msubtype,
372
+ param, content_id,
373
+ desc, enc, size,
374
+ md5, disposition, language, extension)
375
+ end
376
+
377
+ def body_type_text
378
+ mtype, msubtype = media_type
379
+ match(T_SPACE)
380
+ param, content_id, desc, enc, size = body_fields
381
+ match(T_SPACE)
382
+ lines = number
383
+ md5, disposition, language, extension = body_ext_1part
384
+ return BodyTypeText.new(mtype, msubtype,
385
+ param, content_id,
386
+ desc, enc, size,
387
+ lines,
388
+ md5, disposition, language, extension)
389
+ end
390
+
391
+ def body_type_msg
392
+ mtype, msubtype = media_type
393
+ match(T_SPACE)
394
+ param, content_id, desc, enc, size = body_fields
395
+
396
+ token = lookahead
397
+ if token.symbol == T_RPAR
398
+ # If this is not message/rfc822, we shouldn't apply the RFC822
399
+ # spec to it. We should handle anything other than
400
+ # message/rfc822 using multipart extension data [rfc3501] (i.e.
401
+ # the data itself won't be returned, we would have to retrieve it
402
+ # with BODYSTRUCTURE instead of with BODY
403
+
404
+ # Also, sometimes a message/rfc822 is included as a large
405
+ # attachment instead of having all of the other details
406
+ # (e.g. attaching a .eml file to an email)
407
+ if msubtype == "RFC822"
408
+ return BodyTypeMessage.new(mtype, msubtype, param, content_id,
409
+ desc, enc, size, nil, nil, nil, nil,
410
+ nil, nil, nil)
411
+ else
412
+ return BodyTypeExtension.new(mtype, msubtype,
413
+ param, content_id,
414
+ desc, enc, size)
415
+ end
416
+ end
417
+
418
+ match(T_SPACE)
419
+ env = envelope
420
+ match(T_SPACE)
421
+ b = body
422
+ match(T_SPACE)
423
+ lines = number
424
+ md5, disposition, language, extension = body_ext_1part
425
+ return BodyTypeMessage.new(mtype, msubtype,
426
+ param, content_id,
427
+ desc, enc, size,
428
+ env, b, lines,
429
+ md5, disposition, language, extension)
430
+ end
431
+
432
+ def body_type_attachment
433
+ mtype = case_insensitive_string
434
+ match(T_SPACE)
435
+ param = body_fld_param
436
+ return BodyTypeAttachment.new(mtype, nil, param)
437
+ end
438
+
439
+ def body_type_mixed
440
+ mtype = "MULTIPART"
441
+ msubtype = case_insensitive_string
442
+ param, disposition, language, extension = body_ext_mpart
443
+ return BodyTypeBasic.new(mtype, msubtype, param, nil, nil, nil, nil, nil, disposition, language, extension)
444
+ end
445
+
446
+ def body_type_mpart
447
+ parts = []
448
+ while true
449
+ token = lookahead
450
+ if token.symbol == T_SPACE
451
+ shift_token
452
+ break
453
+ end
454
+ parts.push(body)
455
+ end
456
+ mtype = "MULTIPART"
457
+ msubtype = case_insensitive_string
458
+ param, disposition, language, extension = body_ext_mpart
459
+ return BodyTypeMultipart.new(mtype, msubtype, parts,
460
+ param, disposition, language,
461
+ extension)
462
+ end
463
+
464
+ def media_type
465
+ mtype = case_insensitive_string
466
+ token = lookahead
467
+ if token.symbol != T_SPACE
468
+ return mtype, nil
469
+ end
470
+ match(T_SPACE)
471
+ msubtype = case_insensitive_string
472
+ return mtype, msubtype
473
+ end
474
+
475
+ def body_fields
476
+ param = body_fld_param
477
+ match(T_SPACE)
478
+ content_id = nstring
479
+ match(T_SPACE)
480
+ desc = nstring
481
+ match(T_SPACE)
482
+ enc = case_insensitive_string
483
+ match(T_SPACE)
484
+ size = number
485
+ return param, content_id, desc, enc, size
486
+ end
487
+
488
+ def body_fld_param
489
+ token = lookahead
490
+ if token.symbol == T_NIL
491
+ shift_token
492
+ return nil
493
+ end
494
+ match(T_LPAR)
495
+ param = {}
496
+ while true
497
+ token = lookahead
498
+ case token.symbol
499
+ when T_RPAR
500
+ shift_token
501
+ break
502
+ when T_SPACE
503
+ shift_token
504
+ end
505
+ name = case_insensitive_string
506
+ match(T_SPACE)
507
+ val = string
508
+ param[name] = val
509
+ end
510
+ return param
511
+ end
512
+
513
+ def body_ext_1part
514
+ token = lookahead
515
+ if token.symbol == T_SPACE
516
+ shift_token
517
+ else
518
+ return nil
519
+ end
520
+ md5 = nstring
521
+
522
+ token = lookahead
523
+ if token.symbol == T_SPACE
524
+ shift_token
525
+ else
526
+ return md5
527
+ end
528
+ disposition = body_fld_dsp
529
+
530
+ token = lookahead
531
+ if token.symbol == T_SPACE
532
+ shift_token
533
+ else
534
+ return md5, disposition
535
+ end
536
+ language = body_fld_lang
537
+
538
+ token = lookahead
539
+ if token.symbol == T_SPACE
540
+ shift_token
541
+ else
542
+ return md5, disposition, language
543
+ end
544
+
545
+ extension = body_extensions
546
+ return md5, disposition, language, extension
547
+ end
548
+
549
+ def body_ext_mpart
550
+ token = lookahead
551
+ if token.symbol == T_SPACE
552
+ shift_token
553
+ else
554
+ return nil
555
+ end
556
+ param = body_fld_param
557
+
558
+ token = lookahead
559
+ if token.symbol == T_SPACE
560
+ shift_token
561
+ else
562
+ return param
563
+ end
564
+ disposition = body_fld_dsp
565
+
566
+ token = lookahead
567
+ if token.symbol == T_SPACE
568
+ shift_token
569
+ else
570
+ return param, disposition
571
+ end
572
+ language = body_fld_lang
573
+
574
+ token = lookahead
575
+ if token.symbol == T_SPACE
576
+ shift_token
577
+ else
578
+ return param, disposition, language
579
+ end
580
+
581
+ extension = body_extensions
582
+ return param, disposition, language, extension
583
+ end
584
+
585
+ def body_fld_dsp
586
+ token = lookahead
587
+ if token.symbol == T_NIL
588
+ shift_token
589
+ return nil
590
+ end
591
+ match(T_LPAR)
592
+ dsp_type = case_insensitive_string
593
+ match(T_SPACE)
594
+ param = body_fld_param
595
+ match(T_RPAR)
596
+ return ContentDisposition.new(dsp_type, param)
597
+ end
598
+
599
+ def body_fld_lang
600
+ token = lookahead
601
+ if token.symbol == T_LPAR
602
+ shift_token
603
+ result = []
604
+ while true
605
+ token = lookahead
606
+ case token.symbol
607
+ when T_RPAR
608
+ shift_token
609
+ return result
610
+ when T_SPACE
611
+ shift_token
612
+ end
613
+ result.push(case_insensitive_string)
614
+ end
615
+ else
616
+ lang = nstring
617
+ if lang
618
+ return lang.upcase
619
+ else
620
+ return lang
621
+ end
622
+ end
623
+ end
624
+
625
+ def body_extensions
626
+ result = []
627
+ while true
628
+ token = lookahead
629
+ case token.symbol
630
+ when T_RPAR
631
+ return result
632
+ when T_SPACE
633
+ shift_token
634
+ end
635
+ result.push(body_extension)
636
+ end
637
+ end
638
+
639
+ def body_extension
640
+ token = lookahead
641
+ case token.symbol
642
+ when T_LPAR
643
+ shift_token
644
+ result = body_extensions
645
+ match(T_RPAR)
646
+ return result
647
+ when T_NUMBER
648
+ return number
649
+ else
650
+ return nstring
651
+ end
652
+ end
653
+
654
+ def section
655
+ str = String.new
656
+ token = match(T_LBRA)
657
+ str.concat(token.value)
658
+ token = match(T_ATOM, T_NUMBER, T_RBRA)
659
+ if token.symbol == T_RBRA
660
+ str.concat(token.value)
661
+ return str
662
+ end
663
+ str.concat(token.value)
664
+ token = lookahead
665
+ if token.symbol == T_SPACE
666
+ shift_token
667
+ str.concat(token.value)
668
+ token = match(T_LPAR)
669
+ str.concat(token.value)
670
+ while true
671
+ token = lookahead
672
+ case token.symbol
673
+ when T_RPAR
674
+ str.concat(token.value)
675
+ shift_token
676
+ break
677
+ when T_SPACE
678
+ shift_token
679
+ str.concat(token.value)
680
+ end
681
+ str.concat(format_string(astring))
682
+ end
683
+ end
684
+ token = match(T_RBRA)
685
+ str.concat(token.value)
686
+ return str
687
+ end
688
+
689
+ def format_string(str)
690
+ case str
691
+ when ""
692
+ return '""'
693
+ when /[\x80-\xff\r\n]/n
694
+ # literal
695
+ return "{" + str.bytesize.to_s + "}" + CRLF + str
696
+ when /[(){ \x00-\x1f\x7f%*"\\]/n
697
+ # quoted string
698
+ return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
699
+ else
700
+ # atom
701
+ return str
702
+ end
703
+ end
704
+
705
+ def uid_data
706
+ token = match(T_ATOM)
707
+ name = token.value.upcase
708
+ match(T_SPACE)
709
+ return name, number
710
+ end
711
+
712
+ def modseq_data
713
+ token = match(T_ATOM)
714
+ name = token.value.upcase
715
+ match(T_SPACE)
716
+ match(T_LPAR)
717
+ modseq = number
718
+ match(T_RPAR)
719
+ return name, modseq
720
+ end
721
+
722
+ def ignored_response
723
+ while lookahead.symbol != T_CRLF
724
+ shift_token
725
+ end
726
+ return IgnoredResponse.new(@str)
727
+ end
728
+
729
+ def text_response
730
+ token = match(T_ATOM)
731
+ name = token.value.upcase
732
+ match(T_SPACE)
733
+ return UntaggedResponse.new(name, text)
734
+ end
735
+
736
+ def flags_response
737
+ token = match(T_ATOM)
738
+ name = token.value.upcase
739
+ match(T_SPACE)
740
+ return UntaggedResponse.new(name, flag_list, @str)
741
+ end
742
+
743
+ def list_response
744
+ token = match(T_ATOM)
745
+ name = token.value.upcase
746
+ match(T_SPACE)
747
+ return UntaggedResponse.new(name, mailbox_list, @str)
748
+ end
749
+
750
+ def mailbox_list
751
+ attr = flag_list
752
+ match(T_SPACE)
753
+ token = match(T_QUOTED, T_NIL)
754
+ if token.symbol == T_NIL
755
+ delim = nil
756
+ else
757
+ delim = token.value
758
+ end
759
+ match(T_SPACE)
760
+ name = astring
761
+ return MailboxList.new(attr, delim, name)
762
+ end
763
+
764
+ def getquota_response
765
+ # If quota never established, get back
766
+ # `NO Quota root does not exist'.
767
+ # If quota removed, get `()' after the
768
+ # folder spec with no mention of `STORAGE'.
769
+ token = match(T_ATOM)
770
+ name = token.value.upcase
771
+ match(T_SPACE)
772
+ mailbox = astring
773
+ match(T_SPACE)
774
+ match(T_LPAR)
775
+ token = lookahead
776
+ case token.symbol
777
+ when T_RPAR
778
+ shift_token
779
+ data = MailboxQuota.new(mailbox, nil, nil)
780
+ return UntaggedResponse.new(name, data, @str)
781
+ when T_ATOM
782
+ shift_token
783
+ match(T_SPACE)
784
+ token = match(T_NUMBER)
785
+ usage = token.value
786
+ match(T_SPACE)
787
+ token = match(T_NUMBER)
788
+ quota = token.value
789
+ match(T_RPAR)
790
+ data = MailboxQuota.new(mailbox, usage, quota)
791
+ return UntaggedResponse.new(name, data, @str)
792
+ else
793
+ parse_error("unexpected token %s", token.symbol)
794
+ end
795
+ end
796
+
797
+ def getquotaroot_response
798
+ # Similar to getquota, but only admin can use getquota.
799
+ token = match(T_ATOM)
800
+ name = token.value.upcase
801
+ match(T_SPACE)
802
+ mailbox = astring
803
+ quotaroots = []
804
+ while true
805
+ token = lookahead
806
+ break unless token.symbol == T_SPACE
807
+ shift_token
808
+ quotaroots.push(astring)
809
+ end
810
+ data = MailboxQuotaRoot.new(mailbox, quotaroots)
811
+ return UntaggedResponse.new(name, data, @str)
812
+ end
813
+
814
+ def getacl_response
815
+ token = match(T_ATOM)
816
+ name = token.value.upcase
817
+ match(T_SPACE)
818
+ mailbox = astring
819
+ data = []
820
+ token = lookahead
821
+ if token.symbol == T_SPACE
822
+ shift_token
823
+ while true
824
+ token = lookahead
825
+ case token.symbol
826
+ when T_CRLF
827
+ break
828
+ when T_SPACE
829
+ shift_token
830
+ end
831
+ user = astring
832
+ match(T_SPACE)
833
+ rights = astring
834
+ data.push(MailboxACLItem.new(user, rights, mailbox))
835
+ end
836
+ end
837
+ return UntaggedResponse.new(name, data, @str)
838
+ end
839
+
840
+ def search_response
841
+ token = match(T_ATOM)
842
+ name = token.value.upcase
843
+ token = lookahead
844
+ if token.symbol == T_SPACE
845
+ shift_token
846
+ data = []
847
+ while true
848
+ token = lookahead
849
+ case token.symbol
850
+ when T_CRLF
851
+ break
852
+ when T_SPACE
853
+ shift_token
854
+ when T_NUMBER
855
+ data.push(number)
856
+ when T_LPAR
857
+ # TODO: include the MODSEQ value in a response
858
+ shift_token
859
+ match(T_ATOM)
860
+ match(T_SPACE)
861
+ match(T_NUMBER)
862
+ match(T_RPAR)
863
+ end
864
+ end
865
+ else
866
+ data = []
867
+ end
868
+ return UntaggedResponse.new(name, data, @str)
869
+ end
870
+
871
+ def thread_response
872
+ token = match(T_ATOM)
873
+ name = token.value.upcase
874
+ token = lookahead
875
+
876
+ if token.symbol == T_SPACE
877
+ threads = []
878
+
879
+ while true
880
+ shift_token
881
+ token = lookahead
882
+
883
+ case token.symbol
884
+ when T_LPAR
885
+ threads << thread_branch(token)
886
+ when T_CRLF
887
+ break
888
+ end
889
+ end
890
+ else
891
+ # no member
892
+ threads = []
893
+ end
894
+
895
+ return UntaggedResponse.new(name, threads, @str)
896
+ end
897
+
898
+ def thread_branch(token)
899
+ rootmember = nil
900
+ lastmember = nil
901
+
902
+ while true
903
+ shift_token # ignore first T_LPAR
904
+ token = lookahead
905
+
906
+ case token.symbol
907
+ when T_NUMBER
908
+ # new member
909
+ newmember = ThreadMember.new(number, [])
910
+ if rootmember.nil?
911
+ rootmember = newmember
912
+ else
913
+ lastmember.children << newmember
914
+ end
915
+ lastmember = newmember
916
+ when T_SPACE
917
+ # do nothing
918
+ when T_LPAR
919
+ if rootmember.nil?
920
+ # dummy member
921
+ lastmember = rootmember = ThreadMember.new(nil, [])
922
+ end
923
+
924
+ lastmember.children << thread_branch(token)
925
+ when T_RPAR
926
+ break
927
+ end
928
+ end
929
+
930
+ return rootmember
931
+ end
932
+
933
+ def status_response
934
+ token = match(T_ATOM)
935
+ name = token.value.upcase
936
+ match(T_SPACE)
937
+ mailbox = astring
938
+ match(T_SPACE)
939
+ match(T_LPAR)
940
+ attr = {}
941
+ while true
942
+ token = lookahead
943
+ case token.symbol
944
+ when T_RPAR
945
+ shift_token
946
+ break
947
+ when T_SPACE
948
+ shift_token
949
+ end
950
+ token = match(T_ATOM)
951
+ key = token.value.upcase
952
+ match(T_SPACE)
953
+ val = number
954
+ attr[key] = val
955
+ end
956
+ data = StatusData.new(mailbox, attr)
957
+ return UntaggedResponse.new(name, data, @str)
958
+ end
959
+
960
+ def capability_response
961
+ token = match(T_ATOM)
962
+ name = token.value.upcase
963
+ match(T_SPACE)
964
+ UntaggedResponse.new(name, capability_data, @str)
965
+ end
966
+
967
+ def capability_data
968
+ data = []
969
+ while true
970
+ token = lookahead
971
+ case token.symbol
972
+ when T_CRLF, T_RBRA
973
+ break
974
+ when T_SPACE
975
+ shift_token
976
+ next
977
+ end
978
+ data.push(atom.upcase)
979
+ end
980
+ data
981
+ end
982
+
983
+ def id_response
984
+ token = match(T_ATOM)
985
+ name = token.value.upcase
986
+ match(T_SPACE)
987
+ token = match(T_LPAR, T_NIL)
988
+ if token.symbol == T_NIL
989
+ return UntaggedResponse.new(name, nil, @str)
990
+ else
991
+ data = {}
992
+ while true
993
+ token = lookahead
994
+ case token.symbol
995
+ when T_RPAR
996
+ shift_token
997
+ break
998
+ when T_SPACE
999
+ shift_token
1000
+ next
1001
+ else
1002
+ key = string
1003
+ match(T_SPACE)
1004
+ val = nstring
1005
+ data[key] = val
1006
+ end
1007
+ end
1008
+ return UntaggedResponse.new(name, data, @str)
1009
+ end
1010
+ end
1011
+
1012
+ def namespace_response
1013
+ @lex_state = EXPR_DATA
1014
+ token = lookahead
1015
+ token = match(T_ATOM)
1016
+ name = token.value.upcase
1017
+ match(T_SPACE)
1018
+ personal = namespaces
1019
+ match(T_SPACE)
1020
+ other = namespaces
1021
+ match(T_SPACE)
1022
+ shared = namespaces
1023
+ @lex_state = EXPR_BEG
1024
+ data = Namespaces.new(personal, other, shared)
1025
+ return UntaggedResponse.new(name, data, @str)
1026
+ end
1027
+
1028
+ def namespaces
1029
+ token = lookahead
1030
+ # empty () is not allowed, so nil is functionally identical to empty.
1031
+ data = []
1032
+ if token.symbol == T_NIL
1033
+ shift_token
1034
+ else
1035
+ match(T_LPAR)
1036
+ loop do
1037
+ data << namespace
1038
+ break unless lookahead.symbol == T_SPACE
1039
+ shift_token
1040
+ end
1041
+ match(T_RPAR)
1042
+ end
1043
+ data
1044
+ end
1045
+
1046
+ def namespace
1047
+ match(T_LPAR)
1048
+ prefix = match(T_QUOTED, T_LITERAL).value
1049
+ match(T_SPACE)
1050
+ delimiter = string
1051
+ extensions = namespace_response_extensions
1052
+ match(T_RPAR)
1053
+ Namespace.new(prefix, delimiter, extensions)
1054
+ end
1055
+
1056
+ def namespace_response_extensions
1057
+ data = {}
1058
+ token = lookahead
1059
+ if token.symbol == T_SPACE
1060
+ shift_token
1061
+ name = match(T_QUOTED, T_LITERAL).value
1062
+ data[name] ||= []
1063
+ match(T_SPACE)
1064
+ match(T_LPAR)
1065
+ loop do
1066
+ data[name].push match(T_QUOTED, T_LITERAL).value
1067
+ break unless lookahead.symbol == T_SPACE
1068
+ shift_token
1069
+ end
1070
+ match(T_RPAR)
1071
+ end
1072
+ data
1073
+ end
1074
+
1075
+ # text = 1*TEXT-CHAR
1076
+ # TEXT-CHAR = <any CHAR except CR and LF>
1077
+ def text
1078
+ match(T_TEXT, lex_state: EXPR_TEXT).value
1079
+ end
1080
+
1081
+ # resp-text = ["[" resp-text-code "]" SP] text
1082
+ def resp_text
1083
+ token = match(T_LBRA, T_TEXT, lex_state: EXPR_RTEXT)
1084
+ case token.symbol
1085
+ when T_LBRA
1086
+ code = resp_text_code
1087
+ match(T_RBRA)
1088
+ accept_space # violating RFC
1089
+ ResponseText.new(code, text)
1090
+ when T_TEXT
1091
+ ResponseText.new(nil, token.value)
1092
+ end
1093
+ end
1094
+
1095
+ # See https://www.rfc-editor.org/errata/rfc3501
1096
+ #
1097
+ # resp-text-code = "ALERT" /
1098
+ # "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
1099
+ # capability-data / "PARSE" /
1100
+ # "PERMANENTFLAGS" SP "("
1101
+ # [flag-perm *(SP flag-perm)] ")" /
1102
+ # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1103
+ # "UIDNEXT" SP nz-number / "UIDVALIDITY" SP nz-number /
1104
+ # "UNSEEN" SP nz-number /
1105
+ # atom [SP 1*<any TEXT-CHAR except "]">]
1106
+ def resp_text_code
1107
+ token = match(T_ATOM)
1108
+ name = token.value.upcase
1109
+ case name
1110
+ when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
1111
+ result = ResponseCode.new(name, nil)
1112
+ when /\A(?:BADCHARSET)\z/n
1113
+ result = ResponseCode.new(name, charset_list)
1114
+ when /\A(?:CAPABILITY)\z/ni
1115
+ result = ResponseCode.new(name, capability_data)
1116
+ when /\A(?:PERMANENTFLAGS)\z/n
1117
+ match(T_SPACE)
1118
+ result = ResponseCode.new(name, flag_list)
1119
+ when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
1120
+ match(T_SPACE)
1121
+ result = ResponseCode.new(name, number)
1122
+ else
1123
+ token = lookahead
1124
+ if token.symbol == T_SPACE
1125
+ shift_token
1126
+ token = match(T_TEXT, lex_state: EXPR_CTEXT)
1127
+ result = ResponseCode.new(name, token.value)
1128
+ else
1129
+ result = ResponseCode.new(name, nil)
1130
+ end
1131
+ end
1132
+ return result
1133
+ end
1134
+
1135
+ def charset_list
1136
+ result = []
1137
+ if accept(T_SPACE)
1138
+ match(T_LPAR)
1139
+ result << charset
1140
+ while accept(T_SPACE)
1141
+ result << charset
1142
+ end
1143
+ match(T_RPAR)
1144
+ end
1145
+ result
1146
+ end
1147
+
1148
+ def address_list
1149
+ token = lookahead
1150
+ if token.symbol == T_NIL
1151
+ shift_token
1152
+ return nil
1153
+ else
1154
+ result = []
1155
+ match(T_LPAR)
1156
+ while true
1157
+ token = lookahead
1158
+ case token.symbol
1159
+ when T_RPAR
1160
+ shift_token
1161
+ break
1162
+ when T_SPACE
1163
+ shift_token
1164
+ end
1165
+ result.push(address)
1166
+ end
1167
+ return result
1168
+ end
1169
+ end
1170
+
1171
+ ADDRESS_REGEXP = /\G\
1172
+ (?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
1173
+ (?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
1174
+ (?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
1175
+ (?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
1176
+ \)/ni
1177
+
1178
+ def address
1179
+ match(T_LPAR)
1180
+ if @str.index(ADDRESS_REGEXP, @pos)
1181
+ # address does not include literal.
1182
+ @pos = $~.end(0)
1183
+ name = $1
1184
+ route = $2
1185
+ mailbox = $3
1186
+ host = $4
1187
+ for s in [name, route, mailbox, host]
1188
+ if s
1189
+ s.gsub!(/\\(["\\])/n, "\\1")
1190
+ end
1191
+ end
1192
+ else
1193
+ name = nstring
1194
+ match(T_SPACE)
1195
+ route = nstring
1196
+ match(T_SPACE)
1197
+ mailbox = nstring
1198
+ match(T_SPACE)
1199
+ host = nstring
1200
+ match(T_RPAR)
1201
+ end
1202
+ return Address.new(name, route, mailbox, host)
1203
+ end
1204
+
1205
+ FLAG_REGEXP = /\
1206
+ (?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
1207
+ (?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
1208
+
1209
+ def flag_list
1210
+ if @str.index(/\(([^)]*)\)/ni, @pos)
1211
+ @pos = $~.end(0)
1212
+ return $1.scan(FLAG_REGEXP).collect { |flag, atom|
1213
+ if atom
1214
+ atom
1215
+ else
1216
+ flag.capitalize.intern
1217
+ end
1218
+ }
1219
+ else
1220
+ parse_error("invalid flag list")
1221
+ end
1222
+ end
1223
+
1224
+ def nstring
1225
+ token = lookahead
1226
+ if token.symbol == T_NIL
1227
+ shift_token
1228
+ return nil
1229
+ else
1230
+ return string
1231
+ end
1232
+ end
1233
+
1234
+ def astring
1235
+ token = lookahead
1236
+ if string_token?(token)
1237
+ return string
1238
+ else
1239
+ return astring_chars
1240
+ end
1241
+ end
1242
+
1243
+ def string
1244
+ token = lookahead
1245
+ if token.symbol == T_NIL
1246
+ shift_token
1247
+ return nil
1248
+ end
1249
+ token = match(T_QUOTED, T_LITERAL)
1250
+ return token.value
1251
+ end
1252
+
1253
+ STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
1254
+
1255
+ def string_token?(token)
1256
+ return STRING_TOKENS.include?(token.symbol)
1257
+ end
1258
+
1259
+ def case_insensitive_string
1260
+ token = lookahead
1261
+ if token.symbol == T_NIL
1262
+ shift_token
1263
+ return nil
1264
+ end
1265
+ token = match(T_QUOTED, T_LITERAL)
1266
+ return token.value.upcase
1267
+ end
1268
+
1269
+ # atom = 1*ATOM-CHAR
1270
+ # ATOM-CHAR = <any CHAR except atom-specials>
1271
+ ATOM_TOKENS = [
1272
+ T_ATOM,
1273
+ T_NUMBER,
1274
+ T_NIL,
1275
+ T_LBRA,
1276
+ T_PLUS
1277
+ ]
1278
+
1279
+ def atom
1280
+ -combine_adjacent(*ATOM_TOKENS)
1281
+ end
1282
+
1283
+ # ASTRING-CHAR = ATOM-CHAR / resp-specials
1284
+ # resp-specials = "]"
1285
+ ASTRING_CHARS_TOKENS = [*ATOM_TOKENS, T_RBRA]
1286
+
1287
+ def astring_chars
1288
+ combine_adjacent(*ASTRING_CHARS_TOKENS)
1289
+ end
1290
+
1291
+ def combine_adjacent(*tokens)
1292
+ result = "".b
1293
+ while token = accept(*tokens)
1294
+ result << token.value
1295
+ end
1296
+ if result.empty?
1297
+ parse_error('unexpected token %s (expected %s)',
1298
+ lookahead.symbol, args.join(" or "))
1299
+ end
1300
+ result
1301
+ end
1302
+
1303
+ # See https://www.rfc-editor.org/errata/rfc3501
1304
+ #
1305
+ # charset = atom / quoted
1306
+ def charset
1307
+ if token = accept(T_QUOTED)
1308
+ token.value
1309
+ else
1310
+ atom
1311
+ end
1312
+ end
1313
+
1314
+ def number
1315
+ token = lookahead
1316
+ if token.symbol == T_NIL
1317
+ shift_token
1318
+ return nil
1319
+ end
1320
+ token = match(T_NUMBER)
1321
+ return token.value.to_i
1322
+ end
1323
+
1324
+ def nil_atom
1325
+ match(T_NIL)
1326
+ return nil
1327
+ end
1328
+
1329
+ SPACES_REGEXP = /\G */n
1330
+
1331
+ # This advances @pos directly so it's safe before changing @lex_state.
1332
+ def accept_space
1333
+ if @token
1334
+ shift_token if @token.symbol == T_SPACE
1335
+ elsif @str[@pos] == " "
1336
+ @pos += 1
1337
+ end
1338
+ end
1339
+
1340
+ # The RFC is very strict about this and usually we should be too.
1341
+ # But skipping spaces is usually a safe workaround for buggy servers.
1342
+ #
1343
+ # This advances @pos directly so it's safe before changing @lex_state.
1344
+ def accept_spaces
1345
+ shift_token if @token&.symbol == T_SPACE
1346
+ if @str.index(SPACES_REGEXP, @pos)
1347
+ @pos = $~.end(0)
1348
+ end
1349
+ end
1350
+
1351
+ def match(*args, lex_state: @lex_state)
1352
+ if @token && lex_state != @lex_state
1353
+ parse_error("invalid lex_state change to %s with unconsumed token",
1354
+ lex_state)
1355
+ end
1356
+ begin
1357
+ @lex_state, original_lex_state = lex_state, @lex_state
1358
+ token = lookahead
1359
+ unless args.include?(token.symbol)
1360
+ parse_error('unexpected token %s (expected %s)',
1361
+ token.symbol.id2name,
1362
+ args.collect {|i| i.id2name}.join(" or "))
1363
+ end
1364
+ shift_token
1365
+ return token
1366
+ ensure
1367
+ @lex_state = original_lex_state
1368
+ end
1369
+ end
1370
+
1371
+ # like match, but does not raise error on failure.
1372
+ #
1373
+ # returns and shifts token on successful match
1374
+ # returns nil and leaves @token unshifted on no match
1375
+ def accept(*args)
1376
+ token = lookahead
1377
+ if args.include?(token.symbol)
1378
+ shift_token
1379
+ token
1380
+ end
1381
+ end
1382
+
1383
+ def lookahead
1384
+ @token ||= next_token
1385
+ end
1386
+
1387
+ def shift_token
1388
+ @token = nil
1389
+ end
1390
+
1391
+ def next_token
1392
+ case @lex_state
1393
+ when EXPR_BEG
1394
+ if @str.index(BEG_REGEXP, @pos)
1395
+ @pos = $~.end(0)
1396
+ if $1
1397
+ return Token.new(T_SPACE, $+)
1398
+ elsif $2
1399
+ return Token.new(T_NIL, $+)
1400
+ elsif $3
1401
+ return Token.new(T_NUMBER, $+)
1402
+ elsif $4
1403
+ return Token.new(T_ATOM, $+)
1404
+ elsif $5
1405
+ return Token.new(T_QUOTED,
1406
+ $+.gsub(/\\(["\\])/n, "\\1"))
1407
+ elsif $6
1408
+ return Token.new(T_LPAR, $+)
1409
+ elsif $7
1410
+ return Token.new(T_RPAR, $+)
1411
+ elsif $8
1412
+ return Token.new(T_BSLASH, $+)
1413
+ elsif $9
1414
+ return Token.new(T_STAR, $+)
1415
+ elsif $10
1416
+ return Token.new(T_LBRA, $+)
1417
+ elsif $11
1418
+ return Token.new(T_RBRA, $+)
1419
+ elsif $12
1420
+ len = $+.to_i
1421
+ val = @str[@pos, len]
1422
+ @pos += len
1423
+ return Token.new(T_LITERAL, val)
1424
+ elsif $13
1425
+ return Token.new(T_PLUS, $+)
1426
+ elsif $14
1427
+ return Token.new(T_PERCENT, $+)
1428
+ elsif $15
1429
+ return Token.new(T_CRLF, $+)
1430
+ elsif $16
1431
+ return Token.new(T_EOF, $+)
1432
+ else
1433
+ parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
1434
+ end
1435
+ else
1436
+ @str.index(/\S*/n, @pos)
1437
+ parse_error("unknown token - %s", $&.dump)
1438
+ end
1439
+ when EXPR_DATA
1440
+ if @str.index(DATA_REGEXP, @pos)
1441
+ @pos = $~.end(0)
1442
+ if $1
1443
+ return Token.new(T_SPACE, $+)
1444
+ elsif $2
1445
+ return Token.new(T_NIL, $+)
1446
+ elsif $3
1447
+ return Token.new(T_NUMBER, $+)
1448
+ elsif $4
1449
+ return Token.new(T_QUOTED,
1450
+ $+.gsub(/\\(["\\])/n, "\\1"))
1451
+ elsif $5
1452
+ len = $+.to_i
1453
+ val = @str[@pos, len]
1454
+ @pos += len
1455
+ return Token.new(T_LITERAL, val)
1456
+ elsif $6
1457
+ return Token.new(T_LPAR, $+)
1458
+ elsif $7
1459
+ return Token.new(T_RPAR, $+)
1460
+ else
1461
+ parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid")
1462
+ end
1463
+ else
1464
+ @str.index(/\S*/n, @pos)
1465
+ parse_error("unknown token - %s", $&.dump)
1466
+ end
1467
+ when EXPR_TEXT
1468
+ if @str.index(TEXT_REGEXP, @pos)
1469
+ @pos = $~.end(0)
1470
+ if $1
1471
+ return Token.new(T_TEXT, $+)
1472
+ else
1473
+ parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
1474
+ end
1475
+ else
1476
+ @str.index(/\S*/n, @pos)
1477
+ parse_error("unknown token - %s", $&.dump)
1478
+ end
1479
+ when EXPR_RTEXT
1480
+ if @str.index(RTEXT_REGEXP, @pos)
1481
+ @pos = $~.end(0)
1482
+ if $1
1483
+ return Token.new(T_LBRA, $+)
1484
+ elsif $2
1485
+ return Token.new(T_TEXT, $+)
1486
+ else
1487
+ parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
1488
+ end
1489
+ else
1490
+ @str.index(/\S*/n, @pos)
1491
+ parse_error("unknown token - %s", $&.dump)
1492
+ end
1493
+ when EXPR_CTEXT
1494
+ if @str.index(CTEXT_REGEXP, @pos)
1495
+ @pos = $~.end(0)
1496
+ if $1
1497
+ return Token.new(T_TEXT, $+)
1498
+ else
1499
+ parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
1500
+ end
1501
+ else
1502
+ @str.index(/\S*/n, @pos) #/
1503
+ parse_error("unknown token - %s", $&.dump)
1504
+ end
1505
+ else
1506
+ parse_error("invalid @lex_state - %s", @lex_state.inspect)
1507
+ end
1508
+ end
1509
+
1510
+ def parse_error(fmt, *args)
1511
+ if IMAP.debug
1512
+ $stderr.printf("@str: %s\n", @str.dump)
1513
+ $stderr.printf("@pos: %d\n", @pos)
1514
+ $stderr.printf("@lex_state: %s\n", @lex_state)
1515
+ if @token
1516
+ $stderr.printf("@token.symbol: %s\n", @token.symbol)
1517
+ $stderr.printf("@token.value: %s\n", @token.value.inspect)
1518
+ end
1519
+ end
1520
+ raise ResponseParseError, format(fmt, *args)
1521
+ end
1522
+ end
1523
+
1524
+ end
1525
+
1526
+ end