net-imap 0.3.7 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) 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 +1 -0
  5. data/Gemfile +3 -0
  6. data/README.md +15 -4
  7. data/Rakefile +0 -7
  8. data/lib/net/imap/authenticators.rb +26 -57
  9. data/lib/net/imap/command_data.rb +13 -6
  10. data/lib/net/imap/deprecated_client_options.rb +139 -0
  11. data/lib/net/imap/errors.rb +20 -0
  12. data/lib/net/imap/response_data.rb +92 -47
  13. data/lib/net/imap/response_parser/parser_utils.rb +240 -0
  14. data/lib/net/imap/response_parser.rb +1265 -986
  15. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  16. data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
  17. data/lib/net/imap/sasl/authenticators.rb +118 -0
  18. data/lib/net/imap/sasl/client_adapter.rb +72 -0
  19. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +21 -11
  20. data/lib/net/imap/sasl/digest_md5_authenticator.rb +180 -0
  21. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  22. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  23. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +25 -16
  24. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  25. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  26. data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
  27. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  28. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  29. data/lib/net/imap/sasl/stringprep.rb +6 -66
  30. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  31. data/lib/net/imap/sasl.rb +144 -43
  32. data/lib/net/imap/sasl_adapter.rb +21 -0
  33. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  34. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  35. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  36. data/lib/net/imap/stringprep/tables.rb +146 -0
  37. data/lib/net/imap/stringprep/trace.rb +85 -0
  38. data/lib/net/imap/stringprep.rb +159 -0
  39. data/lib/net/imap.rb +993 -609
  40. data/net-imap.gemspec +4 -3
  41. data/rakelib/benchmarks.rake +98 -0
  42. data/rakelib/saslprep.rake +4 -4
  43. data/rakelib/string_prep_tables_generator.rb +82 -60
  44. metadata +29 -13
  45. data/benchmarks/stringprep.yml +0 -65
  46. data/benchmarks/table-regexps.yml +0 -39
  47. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  48. data/lib/net/imap/authenticators/plain.rb +0 -41
  49. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  50. data/lib/net/imap/sasl/saslprep.rb +0 -55
  51. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  52. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -55,17 +55,54 @@ module Net
55
55
 
56
56
  # Net::IMAP::IgnoredResponse represents intentionally ignored responses.
57
57
  #
58
- # This includes untagged response "NOOP" sent by eg. Zimbra to avoid some
59
- # clients to close the connection.
58
+ # This includes untagged response "NOOP" sent by eg. Zimbra to avoid
59
+ # some clients to close the connection.
60
60
  #
61
61
  # It matches no IMAP standard.
62
+ class IgnoredResponse < UntaggedResponse
63
+ end
64
+
65
+ # **Note:** This represents an intentionally _unstable_ API. Where
66
+ # instances of this class are returned, future releases may return a
67
+ # different (incompatible) object <em>without deprecation or warning</em>.
62
68
  #
63
- class IgnoredResponse < Struct.new(:raw_data)
69
+ # Net::IMAP::UnparsedData represents data for unknown response types or
70
+ # unknown extensions to response types without a well-defined extension
71
+ # grammar.
72
+ #
73
+ # See also: UnparsedNumericResponseData
74
+ class UnparsedData < Struct.new(:unparsed_data)
64
75
  ##
65
- # method: raw_data
66
- # :call-seq: raw_data -> string
76
+ # method: unparsed_data
77
+ # :call-seq: unparsed_data -> string
67
78
  #
68
- # The raw response data.
79
+ # The unparsed data
80
+ end
81
+
82
+ # **Note:** This represents an intentionally _unstable_ API. Where
83
+ # instances of this class are returned, future releases may return a
84
+ # different (incompatible) object <em>without deprecation or warning</em>.
85
+ #
86
+ # Net::IMAP::UnparsedNumericResponseData represents data for unhandled
87
+ # response types with a numeric prefix. See the documentation for #number.
88
+ #
89
+ # See also: UnparsedData
90
+ class UnparsedNumericResponseData < Struct.new(:number, :unparsed_data)
91
+ ##
92
+ # method: number
93
+ # :call-seq: number -> integer
94
+ #
95
+ # Returns a numeric response data prefix, when available.
96
+ #
97
+ # Many response types are prefixed with a non-negative #number. For
98
+ # message data, #number may represent a sequence number or a UID. For
99
+ # mailbox data, #number may represent a message count.
100
+
101
+ ##
102
+ # method: unparsed_data
103
+ # :call-seq: unparsed_data -> string
104
+ #
105
+ # The unparsed data, not including #number or UntaggedResponse#name.
69
106
  end
70
107
 
71
108
  # Net::IMAP::TaggedResponse represents tagged responses.
@@ -108,6 +145,9 @@ module Net
108
145
  # UntaggedResponse#data when the response type is a "condition" ("OK", "NO",
109
146
  # "BAD", "PREAUTH", or "BYE").
110
147
  class ResponseText < Struct.new(:code, :text)
148
+ # Used to avoid an allocation when ResponseText is empty
149
+ EMPTY = new(nil, "").freeze
150
+
111
151
  ##
112
152
  # method: code
113
153
  # :call-seq: code -> ResponseCode or nil
@@ -891,13 +931,6 @@ module Net
891
931
  # should use BodyTypeBasic.
892
932
  # BodyTypeMultipart:: for <tt>multipart/*</tt> parts
893
933
  #
894
- # ==== Deprecated BodyStructure classes
895
- # The following classes represent invalid server responses or parser bugs:
896
- # BodyTypeExtension:: parser bug: used for <tt>message/*</tt> where
897
- # BodyTypeBasic should have been used.
898
- # BodyTypeAttachment:: server bug: some servers sometimes return the
899
- # "Content-Disposition: attachment" data where the
900
- # entire body structure for a message part is expected.
901
934
  module BodyStructure
902
935
  end
903
936
 
@@ -914,6 +947,7 @@ module Net
914
947
  :param, :content_id,
915
948
  :description, :encoding, :size,
916
949
  :md5, :disposition, :language,
950
+ :location,
917
951
  :extension)
918
952
  include BodyStructure
919
953
 
@@ -1049,6 +1083,7 @@ module Net
1049
1083
  :description, :encoding, :size,
1050
1084
  :lines,
1051
1085
  :md5, :disposition, :language,
1086
+ :location,
1052
1087
  :extension)
1053
1088
  include BodyStructure
1054
1089
 
@@ -1094,6 +1129,7 @@ module Net
1094
1129
  :description, :encoding, :size,
1095
1130
  :envelope, :body, :lines,
1096
1131
  :md5, :disposition, :language,
1132
+ :location,
1097
1133
  :extension)
1098
1134
  include BodyStructure
1099
1135
 
@@ -1126,36 +1162,41 @@ module Net
1126
1162
  end
1127
1163
  end
1128
1164
 
1129
- # === WARNING
1130
- # BodyTypeAttachment represents a <tt>body-fld-dsp</tt> that is
1131
- # incorrectly in a position where the IMAP4rev1 grammar expects a nested
1132
- # +body+ structure.
1165
+ # BodyTypeAttachment is not used and will be removed in an upcoming release.
1133
1166
  #
1134
- # >>>
1135
- # \IMAP body structures are parenthesized lists and assign their fields
1136
- # positionally, so missing fields change the intepretation of all
1137
- # following fields. Buggy \IMAP servers sometimes leave fields missing
1138
- # rather than empty, which inevitably confuses parsers.
1139
- # BodyTypeAttachment was an attempt to parse a common type of buggy body
1140
- # structure without crashing.
1141
- #
1142
- # Currently, when Net::IMAP::ResponseParser sees "attachment" as the first
1143
- # entry in a <tt>body-type-1part</tt>, which is where the MIME type should
1144
- # be, it uses BodyTypeAttachment to capture the rest. "attachment" is not
1145
- # a valid MIME type, but _is_ a common <tt>Content-Disposition</tt>. What
1146
- # might have happened was that buggy server could not parse the message
1147
- # (which might have been incorrectly formatted) and output a
1148
- # <tt>body-type-dsp</tt> where a Net::IMAP::ResponseParser expected to see
1149
- # a +body+.
1150
- #
1151
- # A future release will replace this, probably with a ContentDisposition
1152
- # nested inside another body structure object, maybe BodyTypeBasic, or
1153
- # perhaps a new body structure class that represents any unparsable body
1154
- # structure.
1167
+ # === Bug Analysis
1168
+ #
1169
+ # \IMAP body structures are parenthesized lists and assign their fields
1170
+ # positionally, so missing fields change the intepretation of all
1171
+ # following fields. Additionally, different body types have a different
1172
+ # number of required fields, followed by optional "extension" fields.
1173
+ #
1174
+ # BodyTypeAttachment was previously returned when a "message/rfc822" part,
1175
+ # which should be sent as <tt>body-type-msg</tt> with ten required fields,
1176
+ # was actually sent as a <tt>body-type-basic</tt> with _seven_ required
1177
+ # fields.
1178
+ #
1179
+ # basic => type, subtype, param, id, desc, enc, octets, md5=nil, dsp=nil, lang=nil, loc=nil, *ext
1180
+ # msg => type, subtype, param, id, desc, enc, octets, envelope, body, lines, md5=nil, ...
1181
+ #
1182
+ # Normally, +envelope+ and +md5+ are incompatible, but Net::IMAP leniently
1183
+ # allowed buggy servers to send +NIL+ for +envelope+. As a result, when a
1184
+ # server sent a <tt>message/rfc822</tt> part with +NIL+ for +md5+ and a
1185
+ # non-<tt>NIL</tt> +dsp+, Net::IMAP mis-interpreted the
1186
+ # <tt>Content-Disposition</tt> as if it were a strange body type. In all
1187
+ # reported cases, the <tt>Content-Disposition</tt> was "attachment", so
1188
+ # BodyTypeAttachment was created as the workaround.
1189
+ #
1190
+ # === Current behavior
1191
+ #
1192
+ # When interpreted strictly, +envelope+ and +md5+ are incompatible. So the
1193
+ # current parsing algorithm peeks ahead after it has recieved the seventh
1194
+ # body field. If the next token is not the start of an +envelope+, we assume
1195
+ # the server has incorrectly sent us a <tt>body-type-basic</tt> and return
1196
+ # BodyTypeBasic. As a result, what was previously BodyTypeMessage#body =>
1197
+ # BodyTypeAttachment is now BodyTypeBasic#disposition => ContentDisposition.
1155
1198
  #
1156
1199
  class BodyTypeAttachment < Struct.new(:dsp_type, :_unused_, :param)
1157
- include BodyStructure
1158
-
1159
1200
  # *invalid for BodyTypeAttachment*
1160
1201
  def media_type
1161
1202
  warn(<<~WARN, uplevel: 1)
@@ -1190,11 +1231,14 @@ module Net
1190
1231
  end
1191
1232
  end
1192
1233
 
1234
+ deprecate_constant :BodyTypeAttachment
1235
+
1193
1236
  # Net::IMAP::BodyTypeMultipart represents body structures of messages and
1194
1237
  # message parts, when <tt>Content-Type</tt> is <tt>multipart/*</tt>.
1195
1238
  class BodyTypeMultipart < Struct.new(:media_type, :subtype,
1196
1239
  :parts,
1197
1240
  :param, :disposition, :language,
1241
+ :location,
1198
1242
  :extension)
1199
1243
  include BodyStructure
1200
1244
 
@@ -1265,23 +1309,24 @@ module Net
1265
1309
  end
1266
1310
  end
1267
1311
 
1268
- # === WARNING:
1312
+ # === Obsolete
1313
+ # BodyTypeExtension is not used and will be removed in an upcoming release.
1314
+ #
1269
1315
  # >>>
1270
- # BodyTypeExtension is (incorrectly) used for <tt>message/*</tt> parts
1316
+ # BodyTypeExtension was (incorrectly) used for <tt>message/*</tt> parts
1271
1317
  # (besides <tt>message/rfc822</tt>, which correctly uses BodyTypeMessage).
1272
1318
  #
1273
- # A future release will replace this class with:
1274
- # * BodyTypeMessage for <tt>message/rfc822</tt> and <tt>message/global</tt>
1275
- # * BodyTypeBasic for any other <tt>message/*</tt>
1319
+ # Net::IMAP now (correctly) parses all message types (other than
1320
+ # <tt>message/rfc822</tt> or <tt>message/global</tt>) as BodyTypeBasic.
1276
1321
  class BodyTypeExtension < Struct.new(:media_type, :subtype,
1277
1322
  :params, :content_id,
1278
1323
  :description, :encoding, :size)
1279
- include BodyStructure
1280
-
1281
1324
  def multipart?
1282
1325
  return false
1283
1326
  end
1284
1327
  end
1285
1328
 
1329
+ deprecate_constant :BodyTypeExtension
1330
+
1286
1331
  end
1287
1332
  end
@@ -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
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