net-imap 0.3.7 → 0.4.4

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 (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