net-imap 0.3.7 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) 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/.gitignore +2 -0
  5. data/Gemfile +3 -0
  6. data/README.md +15 -4
  7. data/Rakefile +0 -7
  8. data/docs/styles.css +0 -12
  9. data/lib/net/imap/authenticators.rb +26 -57
  10. data/lib/net/imap/command_data.rb +13 -6
  11. data/lib/net/imap/data_encoding.rb +14 -2
  12. data/lib/net/imap/deprecated_client_options.rb +139 -0
  13. data/lib/net/imap/errors.rb +20 -0
  14. data/lib/net/imap/fetch_data.rb +518 -0
  15. data/lib/net/imap/response_data.rb +116 -252
  16. data/lib/net/imap/response_parser/parser_utils.rb +240 -0
  17. data/lib/net/imap/response_parser.rb +1535 -1003
  18. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  19. data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
  20. data/lib/net/imap/sasl/authenticators.rb +118 -0
  21. data/lib/net/imap/sasl/client_adapter.rb +72 -0
  22. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +21 -11
  23. data/lib/net/imap/sasl/digest_md5_authenticator.rb +180 -0
  24. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  25. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  26. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +25 -16
  27. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  28. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  29. data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
  30. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  31. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  32. data/lib/net/imap/sasl/stringprep.rb +6 -66
  33. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  34. data/lib/net/imap/sasl.rb +144 -43
  35. data/lib/net/imap/sasl_adapter.rb +21 -0
  36. data/lib/net/imap/sequence_set.rb +67 -0
  37. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  38. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  39. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  40. data/lib/net/imap/stringprep/tables.rb +146 -0
  41. data/lib/net/imap/stringprep/trace.rb +85 -0
  42. data/lib/net/imap/stringprep.rb +159 -0
  43. data/lib/net/imap.rb +1055 -612
  44. data/net-imap.gemspec +4 -3
  45. data/rakelib/benchmarks.rake +91 -0
  46. data/rakelib/saslprep.rake +4 -4
  47. data/rakelib/string_prep_tables_generator.rb +82 -60
  48. metadata +31 -13
  49. data/benchmarks/stringprep.yml +0 -65
  50. data/benchmarks/table-regexps.yml +0 -39
  51. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  52. data/lib/net/imap/authenticators/plain.rb +0 -41
  53. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  54. data/lib/net/imap/sasl/saslprep.rb +0 -55
  55. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  56. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,240 @@
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 # :nodoc:
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
+ byte = char.ord
19
+ match_name = name.match(/\A[A-Z]/) ? "#{name}!" : name
20
+ char = char.dump
21
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
22
+ # frozen_string_literal: true
23
+
24
+ # force use of #next_token; no string peeking
25
+ def lookahead_#{name}?
26
+ #{LOOKAHEAD}&.symbol == #{token}
27
+ end
28
+
29
+ # use token or string peek
30
+ def peek_#{name}?
31
+ @token ? @token.symbol == #{token} : @str.getbyte(@pos) == #{byte}
32
+ end
33
+
34
+ # like accept(token_symbols); returns token or nil
35
+ def #{name}?
36
+ if @token&.symbol == #{token}
37
+ #{SHIFT_TOKEN}
38
+ #{char}
39
+ elsif !@token && @str.getbyte(@pos) == #{byte}
40
+ @pos += 1
41
+ #{char}
42
+ end
43
+ end
44
+
45
+ # like match(token_symbols); returns token or raises parse_error
46
+ def #{match_name}
47
+ if @token&.symbol == #{token}
48
+ #{SHIFT_TOKEN}
49
+ #{char}
50
+ elsif !@token && @str.getbyte(@pos) == #{byte}
51
+ @pos += 1
52
+ #{char}
53
+ else
54
+ parse_error("unexpected %s (expected %p)",
55
+ @token&.symbol || @str[@pos].inspect, #{char})
56
+ end
57
+ end
58
+ RUBY
59
+ end
60
+
61
+ # TODO: move coersion to the token.value method?
62
+ def def_token_matchers(name, *token_symbols, coerce: nil, send: nil)
63
+ match_name = name.match(/\A[A-Z]/) ? "#{name}!" : name
64
+
65
+ if token_symbols.size == 1
66
+ token = token_symbols.first
67
+ matcher = "token&.symbol == %p" % [token]
68
+ desc = token
69
+ else
70
+ matcher = "%p.include? token&.symbol" % [token_symbols]
71
+ desc = token_symbols.join(" or ")
72
+ end
73
+
74
+ value = "(token.value)"
75
+ value = coerce.to_s + value if coerce
76
+ value = [value, send].join(".") if send
77
+
78
+ raise_parse_error = <<~RUBY
79
+ parse_error("unexpected %s (expected #{desc})", token&.symbol)
80
+ RUBY
81
+
82
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
83
+ # frozen_string_literal: true
84
+
85
+ # lookahead version of match, returning the value
86
+ def lookahead_#{name}!
87
+ token = #{LOOKAHEAD}
88
+ if #{matcher}
89
+ #{value}
90
+ else
91
+ #{raise_parse_error}
92
+ end
93
+ end
94
+
95
+ def #{name}?
96
+ token = #{LOOKAHEAD}
97
+ if #{matcher}
98
+ #{SHIFT_TOKEN}
99
+ #{value}
100
+ end
101
+ end
102
+
103
+ def #{match_name}
104
+ token = #{LOOKAHEAD}
105
+ if #{matcher}
106
+ #{SHIFT_TOKEN}
107
+ #{value}
108
+ else
109
+ #{raise_parse_error}
110
+ end
111
+ end
112
+ RUBY
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
+ # like match, without consuming the token
174
+ def lookahead!(*args)
175
+ if args.include?((@token ||= next_token)&.symbol)
176
+ @token
177
+ else
178
+ parse_error('unexpected token %s (expected %s)',
179
+ @token&.symbol, args.join(" or "))
180
+ end
181
+ end
182
+
183
+ def peek_str?(str)
184
+ assert_no_lookahead if Net::IMAP.debug
185
+ @str[@pos, str.length] == str
186
+ end
187
+
188
+ def peek_re(re)
189
+ assert_no_lookahead if Net::IMAP.debug
190
+ re.match(@str, @pos)
191
+ end
192
+
193
+ def accept_re(re)
194
+ assert_no_lookahead if Net::IMAP.debug
195
+ re.match(@str, @pos) and @pos = $~.end(0)
196
+ $~
197
+ end
198
+
199
+ def match_re(re, name)
200
+ assert_no_lookahead if Net::IMAP.debug
201
+ if re.match(@str, @pos)
202
+ @pos = $~.end(0)
203
+ $~
204
+ else
205
+ parse_error("invalid #{name}")
206
+ end
207
+ end
208
+
209
+ def shift_token
210
+ @token = nil
211
+ end
212
+
213
+ def parse_error(fmt, *args)
214
+ msg = format(fmt, *args)
215
+ if IMAP.debug
216
+ local_path = File.dirname(__dir__)
217
+ tok = @token ? "%s: %p" % [@token.symbol, @token.value] : "nil"
218
+ warn "%s %s: %s" % [self.class, __method__, msg]
219
+ warn " tokenized : %s" % [@str[...@pos].dump]
220
+ warn " remaining : %s" % [@str[@pos..].dump]
221
+ warn " @lex_state: %s" % [@lex_state]
222
+ warn " @pos : %d" % [@pos]
223
+ warn " @token : %s" % [tok]
224
+ caller_locations(1..20).each_with_index do |cloc, idx|
225
+ next unless cloc.path&.start_with?(local_path)
226
+ warn " caller[%2d]: %-30s (%s:%d)" % [
227
+ idx,
228
+ cloc.base_label,
229
+ File.basename(cloc.path, ".rb"),
230
+ cloc.lineno
231
+ ]
232
+ end
233
+ end
234
+ raise ResponseParseError, msg
235
+ end
236
+
237
+ end
238
+ end
239
+ end
240
+ end