1
1
# frozen_string_literal: true
2
2
3
3
require_relative "errors"
4
+ require_relative "response_parser/parser_utils"
4
5
5
6
module Net
6
7
class IMAP < Protocol
7
8
8
9
# Parses an \IMAP server response.
9
10
class ResponseParser
11
+ include ParserUtils
12
+
10
13
# :call-seq: Net::IMAP::ResponseParser.new -> Net::IMAP::ResponseParser
11
14
def initialize
12
15
@str = nil
@@ -96,6 +99,45 @@ def parse(str)
96
99
97
100
Token = Struct . new ( :symbol , :value )
98
101
102
+ # atom = 1*ATOM-CHAR
103
+ #
104
+ # TODO: match atom entirely by regexp (in the "lexer")
105
+ def atom ; -combine_adjacent ( *ATOM_TOKENS ) end
106
+
107
+ # the #accept version of #atom
108
+ def atom? ; -combine_adjacent ( *ATOM_TOKENS ) if lookahead? ( *ATOM_TOKENS ) end
109
+
110
+ # Returns <tt>atom.upcase</tt>
111
+ def case_insensitive__atom ; -combine_adjacent ( *ATOM_TOKENS ) . upcase end
112
+
113
+ # Returns <tt>atom?&.upcase</tt>
114
+ def case_insensitive__atom?
115
+ -combine_adjacent ( *ATOM_TOKENS ) . upcase if lookahead? ( *ATOM_TOKENS )
116
+ end
117
+
118
+ # In addition to explicitly uses of +tagged-ext-label+, use this to match
119
+ # keywords when the grammar has not provided any extension syntax.
120
+ #
121
+ # Do *not* use this for labels where the grammar specifies extensions
122
+ # can be +atom+, even if all currently defined labels would match. For
123
+ # example response codes in +resp-text-code+.
124
+ #
125
+ # tagged-ext-label = tagged-label-fchar *tagged-label-char
126
+ # ; Is a valid RFC 3501 "atom".
127
+ # tagged-label-fchar = ALPHA / "-" / "_" / "."
128
+ # tagged-label-char = tagged-label-fchar / DIGIT / ":"
129
+ #
130
+ # TODO: add to lexer and only match tagged-ext-label
131
+ alias tagged_ext_label case_insensitive__atom
132
+ alias tagged_ext_label? case_insensitive__atom?
133
+
134
+ # Use #label or #label_in to assert specific known labels
135
+ # (+tagged-ext-label+ only, not +atom+).
136
+ def label ( word )
137
+ ( val = tagged_ext_label ) == word and return val
138
+ parse_error ( "unexpected atom %p, expected %p instead" , val , word )
139
+ end
140
+
99
141
def response
100
142
token = lookahead
101
143
case token . symbol
@@ -157,9 +199,11 @@ def response_untagged
157
199
when /\A (?:STATUS)\z /ni
158
200
return status_response
159
201
when /\A (?:CAPABILITY)\z /ni
160
- return capability_response
202
+ return capability_data__untagged
161
203
when /\A (?:NOOP)\z /ni
162
204
return ignored_response
205
+ when /\A (?:ENABLED)\z /ni
206
+ return enable_data
163
207
else
164
208
return text_response
165
209
end
@@ -967,29 +1011,38 @@ def status_response
967
1011
return UntaggedResponse . new ( name , data , @str )
968
1012
end
969
1013
970
- def capability_response
971
- token = match ( T_ATOM )
972
- name = token . value . upcase
973
- match ( T_SPACE )
974
- UntaggedResponse . new ( name , capability_data , @str )
1014
+ # The presence of "IMAP4rev1" or "IMAP4rev2" is unenforced here.
1015
+ # The grammar rule is used by both response-data and resp-text-code.
1016
+ # But this method only returns UntaggedResponse (response-data).
1017
+ #
1018
+ # RFC3501:
1019
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1"
1020
+ # *(SP capability)
1021
+ # RFC9051:
1022
+ # capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev2"
1023
+ # *(SP capability)
1024
+ def capability_data__untagged
1025
+ UntaggedResponse . new label ( "CAPABILITY" ) , capability__list , @str
975
1026
end
976
1027
977
- def capability_data
978
- data = [ ]
979
- while true
980
- token = lookahead
981
- case token . symbol
982
- when T_CRLF , T_RBRA
983
- break
984
- when T_SPACE
985
- shift_token
986
- next
987
- end
988
- data . push ( atom . upcase )
989
- end
990
- data
1028
+ # enable-data = "ENABLED" *(SP capability)
1029
+ def enable_data
1030
+ UntaggedResponse . new label ( "ENABLED" ) , capability__list , @str
1031
+ end
1032
+
1033
+ # As a workaround for buggy servers, allow a trailing SP:
1034
+ # *(SP capapility) [SP]
1035
+ def capability__list
1036
+ data = [ ] ; while _ = SP? && capability? do data << _ end ; data
991
1037
end
992
1038
1039
+ # capability = ("AUTH=" auth-type) / atom
1040
+ # ; New capabilities MUST begin with "X" or be
1041
+ # ; registered with IANA as standard or
1042
+ # ; standards-track
1043
+ alias capability case_insensitive__atom
1044
+ alias capability? case_insensitive__atom?
1045
+
993
1046
def id_response
994
1047
token = match ( T_ATOM )
995
1048
name = token . value . upcase
@@ -1125,7 +1178,7 @@ def resp_text_code
1125
1178
when /\A (?:BADCHARSET)\z /n
1126
1179
result = ResponseCode . new ( name , charset_list )
1127
1180
when /\A (?:CAPABILITY)\z /ni
1128
- result = ResponseCode . new ( name , capability_data )
1181
+ result = ResponseCode . new ( name , capability__list )
1129
1182
when /\A (?:PERMANENTFLAGS)\z /n
1130
1183
match ( T_SPACE )
1131
1184
result = ResponseCode . new ( name , flag_list )
@@ -1321,10 +1374,6 @@ def case_insensitive_string
1321
1374
T_PLUS
1322
1375
]
1323
1376
1324
- def atom
1325
- -combine_adjacent ( *ATOM_TOKENS )
1326
- end
1327
-
1328
1377
# ASTRING-CHAR = ATOM-CHAR / resp-specials
1329
1378
# resp-specials = "]"
1330
1379
ASTRING_CHARS_TOKENS = [ *ATOM_TOKENS , T_RBRA ]
@@ -1396,12 +1445,18 @@ def nil_atom
1396
1445
# This advances @pos directly so it's safe before changing @lex_state.
1397
1446
def accept_space
1398
1447
if @token
1399
- shift_token if @token . symbol == T_SPACE
1448
+ if @token . symbol == T_SPACE
1449
+ shift_token
1450
+ " "
1451
+ end
1400
1452
elsif @str [ @pos ] == " "
1401
1453
@pos += 1
1454
+ " "
1402
1455
end
1403
1456
end
1404
1457
1458
+ alias SP? accept_space
1459
+
1405
1460
# The RFC is very strict about this and usually we should be too.
1406
1461
# But skipping spaces is usually a safe workaround for buggy servers.
1407
1462
#
@@ -1413,46 +1468,6 @@ def accept_spaces
1413
1468
end
1414
1469
end
1415
1470
1416
- def match ( *args , lex_state : @lex_state )
1417
- if @token && lex_state != @lex_state
1418
- parse_error ( "invalid lex_state change to %s with unconsumed token" ,
1419
- lex_state )
1420
- end
1421
- begin
1422
- @lex_state , original_lex_state = lex_state , @lex_state
1423
- token = lookahead
1424
- unless args . include? ( token . symbol )
1425
- parse_error ( 'unexpected token %s (expected %s)' ,
1426
- token . symbol . id2name ,
1427
- args . collect { |i | i . id2name } . join ( " or " ) )
1428
- end
1429
- shift_token
1430
- return token
1431
- ensure
1432
- @lex_state = original_lex_state
1433
- end
1434
- end
1435
-
1436
- # like match, but does not raise error on failure.
1437
- #
1438
- # returns and shifts token on successful match
1439
- # returns nil and leaves @token unshifted on no match
1440
- def accept ( *args )
1441
- token = lookahead
1442
- if args . include? ( token . symbol )
1443
- shift_token
1444
- token
1445
- end
1446
- end
1447
-
1448
- def lookahead
1449
- @token ||= next_token
1450
- end
1451
-
1452
- def shift_token
1453
- @token = nil
1454
- end
1455
-
1456
1471
def next_token
1457
1472
case @lex_state
1458
1473
when EXPR_BEG
0 commit comments