net-imap 0.2.1 → 0.2.2

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