net-imap 0.1.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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