net-imap 0.3.9 → 0.4.0

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.

Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +46 -0
  3. data/.github/workflows/test.yml +5 -12
  4. data/Gemfile +1 -0
  5. data/README.md +15 -4
  6. data/Rakefile +0 -7
  7. data/benchmarks/generate_parser_benchmarks +52 -0
  8. data/benchmarks/parser.yml +578 -0
  9. data/benchmarks/stringprep.yml +1 -1
  10. data/lib/net/imap/authenticators.rb +26 -57
  11. data/lib/net/imap/command_data.rb +13 -6
  12. data/lib/net/imap/deprecated_client_options.rb +139 -0
  13. data/lib/net/imap/errors.rb +0 -34
  14. data/lib/net/imap/response_data.rb +46 -41
  15. data/lib/net/imap/response_parser/parser_utils.rb +230 -0
  16. data/lib/net/imap/response_parser.rb +667 -649
  17. data/lib/net/imap/sasl/anonymous_authenticator.rb +68 -0
  18. data/lib/net/imap/sasl/authenticators.rb +112 -0
  19. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +15 -9
  20. data/lib/net/imap/{authenticators/digest_md5.rb → sasl/digest_md5_authenticator.rb} +74 -21
  21. data/lib/net/imap/sasl/external_authenticator.rb +62 -0
  22. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  23. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +19 -14
  24. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +164 -0
  25. data/lib/net/imap/sasl/plain_authenticator.rb +93 -0
  26. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  27. data/lib/net/imap/sasl/scram_authenticator.rb +278 -0
  28. data/lib/net/imap/sasl/stringprep.rb +6 -66
  29. data/lib/net/imap/sasl/xoauth2_authenticator.rb +88 -0
  30. data/lib/net/imap/sasl.rb +139 -44
  31. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  32. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  33. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  34. data/lib/net/imap/stringprep/tables.rb +146 -0
  35. data/lib/net/imap/stringprep/trace.rb +85 -0
  36. data/lib/net/imap/stringprep.rb +159 -0
  37. data/lib/net/imap.rb +987 -690
  38. data/net-imap.gemspec +1 -1
  39. data/rakelib/saslprep.rake +4 -4
  40. data/rakelib/string_prep_tables_generator.rb +82 -60
  41. metadata +30 -13
  42. data/lib/net/imap/authenticators/plain.rb +0 -41
  43. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  44. data/lib/net/imap/response_reader.rb +0 -75
  45. data/lib/net/imap/sasl/saslprep.rb +0 -55
  46. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  47. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP < Protocol
5
+ class ResponseParser
6
+ # basic utility methods for parsing.
7
+ #
8
+ # (internal API, subject to change)
9
+ module ParserUtils # :nodoc:
10
+
11
+ module Generator
12
+
13
+ LOOKAHEAD = "(@token ||= next_token)"
14
+ SHIFT_TOKEN = "(@token = nil)"
15
+
16
+ # we can skip lexer for single character matches, as a shortcut
17
+ def def_char_matchers(name, char, token)
18
+ match_name = name.match(/\A[A-Z]/) ? "#{name}!" : name
19
+ char = char.dump
20
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
21
+ # frozen_string_literal: true
22
+
23
+ # force use of #next_token; no string peeking
24
+ def lookahead_#{name}?
25
+ #{LOOKAHEAD}&.symbol == #{token}
26
+ end
27
+
28
+ # use token or string peek
29
+ def peek_#{name}?
30
+ @token ? @token.symbol == #{token} : @str[@pos] == #{char}
31
+ end
32
+
33
+ # like accept(token_symbols); returns token or nil
34
+ def #{name}?
35
+ if @token&.symbol == #{token}
36
+ #{SHIFT_TOKEN}
37
+ #{char}
38
+ elsif !@token && @str[@pos] == #{char}
39
+ @pos += 1
40
+ #{char}
41
+ end
42
+ end
43
+
44
+ # like match(token_symbols); returns token or raises parse_error
45
+ def #{match_name}
46
+ if @token&.symbol == #{token}
47
+ #{SHIFT_TOKEN}
48
+ #{char}
49
+ elsif !@token && @str[@pos] == #{char}
50
+ @pos += 1
51
+ #{char}
52
+ else
53
+ parse_error("unexpected %s (expected %p)",
54
+ @token&.symbol || @str[@pos].inspect, #{char})
55
+ end
56
+ end
57
+ RUBY
58
+ end
59
+
60
+ # TODO: move coersion to the token.value method?
61
+ def def_token_matchers(name, *token_symbols, coerce: nil, send: nil)
62
+ match_name = name.match(/\A[A-Z]/) ? "#{name}!" : name
63
+
64
+ if token_symbols.size == 1
65
+ token = token_symbols.first
66
+ matcher = "token&.symbol == %p" % [token]
67
+ desc = token
68
+ else
69
+ matcher = "%p.include? token&.symbol" % [token_symbols]
70
+ desc = token_symbols.join(" or ")
71
+ end
72
+
73
+ value = "(token.value)"
74
+ value = coerce.to_s + value if coerce
75
+ value = [value, send].join(".") if send
76
+
77
+ raise_parse_error = <<~RUBY
78
+ parse_error("unexpected %s (expected #{desc})", token&.symbol)
79
+ RUBY
80
+
81
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
82
+ # frozen_string_literal: true
83
+
84
+ # lookahead version of match, returning the value
85
+ def lookahead_#{name}!
86
+ token = #{LOOKAHEAD}
87
+ if #{matcher}
88
+ #{value}
89
+ else
90
+ #{raise_parse_error}
91
+ end
92
+ end
93
+
94
+ def #{name}?
95
+ token = #{LOOKAHEAD}
96
+ if #{matcher}
97
+ #{SHIFT_TOKEN}
98
+ #{value}
99
+ end
100
+ end
101
+
102
+ def #{match_name}
103
+ token = #{LOOKAHEAD}
104
+ if #{matcher}
105
+ #{SHIFT_TOKEN}
106
+ #{value}
107
+ else
108
+ #{raise_parse_error}
109
+ end
110
+ end
111
+ RUBY
112
+
113
+ end
114
+
115
+ end
116
+
117
+ private
118
+
119
+ # TODO: after checking the lookahead, use a regexp for remaining chars.
120
+ # That way a loop isn't needed.
121
+ def combine_adjacent(*tokens)
122
+ result = "".b
123
+ while token = accept(*tokens)
124
+ result << token.value
125
+ end
126
+ if result.empty?
127
+ parse_error('unexpected token %s (expected %s)',
128
+ lookahead.symbol, tokens.join(" or "))
129
+ end
130
+ result
131
+ end
132
+
133
+ def match(*args)
134
+ token = lookahead
135
+ unless args.include?(token.symbol)
136
+ parse_error('unexpected token %s (expected %s)',
137
+ token.symbol.id2name,
138
+ args.collect {|i| i.id2name}.join(" or "))
139
+ end
140
+ shift_token
141
+ token
142
+ end
143
+
144
+ # like match, but does not raise error on failure.
145
+ #
146
+ # returns and shifts token on successful match
147
+ # returns nil and leaves @token unshifted on no match
148
+ def accept(*args)
149
+ token = lookahead
150
+ if args.include?(token.symbol)
151
+ shift_token
152
+ token
153
+ end
154
+ end
155
+
156
+ # To be used conditionally:
157
+ # assert_no_lookahead if Net::IMAP.debug
158
+ def assert_no_lookahead
159
+ @token.nil? or
160
+ parse_error("assertion failed: expected @token.nil?, actual %s: %p",
161
+ @token.symbol, @token.value)
162
+ end
163
+
164
+ # like accept, without consuming the token
165
+ def lookahead?(*symbols)
166
+ @token if symbols.include?((@token ||= next_token)&.symbol)
167
+ end
168
+
169
+ def lookahead
170
+ @token ||= next_token
171
+ end
172
+
173
+ def peek_str?(str)
174
+ assert_no_lookahead if Net::IMAP.debug
175
+ @str[@pos, str.length] == str
176
+ end
177
+
178
+ def peek_re(re)
179
+ assert_no_lookahead if Net::IMAP.debug
180
+ re.match(@str, @pos)
181
+ end
182
+
183
+ def accept_re(re)
184
+ assert_no_lookahead if Net::IMAP.debug
185
+ re.match(@str, @pos) and @pos = $~.end(0)
186
+ $~
187
+ end
188
+
189
+ def match_re(re, name)
190
+ assert_no_lookahead if Net::IMAP.debug
191
+ if re.match(@str, @pos)
192
+ @pos = $~.end(0)
193
+ $~
194
+ else
195
+ parse_error("invalid #{name}")
196
+ end
197
+ end
198
+
199
+ def shift_token
200
+ @token = nil
201
+ end
202
+
203
+ def parse_error(fmt, *args)
204
+ msg = format(fmt, *args)
205
+ if IMAP.debug
206
+ local_path = File.dirname(__dir__)
207
+ tok = @token ? "%s: %p" % [@token.symbol, @token.value] : "nil"
208
+ warn "%s %s: %s" % [self.class, __method__, msg]
209
+ warn " tokenized : %s" % [@str[...@pos].dump]
210
+ warn " remaining : %s" % [@str[@pos..].dump]
211
+ warn " @lex_state: %s" % [@lex_state]
212
+ warn " @pos : %d" % [@pos]
213
+ warn " @token : %s" % [tok]
214
+ caller_locations(1..20).each_with_index do |cloc, idx|
215
+ next unless cloc.path&.start_with?(local_path)
216
+ warn " caller[%2d]: %-30s (%s:%d)" % [
217
+ idx,
218
+ cloc.base_label,
219
+ File.basename(cloc.path, ".rb"),
220
+ cloc.lineno
221
+ ]
222
+ end
223
+ end
224
+ raise ResponseParseError, msg
225
+ end
226
+
227
+ end
228
+ end
229
+ end
230
+ end