net-imap 0.2.1 → 0.2.2

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.

@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Registry for SASL authenticators used by Net::IMAP.
4
+ module Net::IMAP::Authenticators
5
+
6
+ # Adds an authenticator for use with Net::IMAP#authenticate. +auth_type+ is the
7
+ # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
8
+ # supported by +authenticator+ (for instance, "+PLAIN+"). The +authenticator+
9
+ # is an object which defines a +#process+ method to handle authentication with
10
+ # the server. See Net::IMAP::PlainAuthenticator, Net::IMAP::LoginAuthenticator,
11
+ # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator for
12
+ # examples.
13
+ #
14
+ # If +auth_type+ refers to an existing authenticator, it will be
15
+ # replaced by the new one.
16
+ def add_authenticator(auth_type, authenticator)
17
+ authenticators[auth_type] = authenticator
18
+ end
19
+
20
+ # Builds an authenticator for Net::IMAP#authenticate. +args+ will be passed
21
+ # directly to the chosen authenticator's +#initialize+.
22
+ def authenticator(auth_type, *args)
23
+ auth_type = auth_type.upcase
24
+ unless authenticators.has_key?(auth_type)
25
+ raise ArgumentError,
26
+ format('unknown auth type - "%s"', auth_type)
27
+ end
28
+ authenticators[auth_type].new(*args)
29
+ end
30
+
31
+ private
32
+
33
+ def authenticators
34
+ @authenticators ||= {}
35
+ end
36
+
37
+ end
38
+
39
+ Net::IMAP.extend Net::IMAP::Authenticators
40
+
41
+ require_relative "authenticators/login"
42
+ require_relative "authenticators/plain"
43
+ require_relative "authenticators/cram_md5"
44
+ require_relative "authenticators/digest_md5"
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+
5
+ # Authenticator for the "+CRAM-MD5+" SASL mechanism, specified in
6
+ # RFC2195[https://tools.ietf.org/html/rfc2195]. See Net::IMAP#authenticate.
7
+ #
8
+ # == Deprecated
9
+ #
10
+ # +CRAM-MD5+ is obsolete and insecure. It is included for compatibility with
11
+ # existing servers.
12
+ # {draft-ietf-sasl-crammd5-to-historic}[https://tools.ietf.org/html/draft-ietf-sasl-crammd5-to-historic-00.html]
13
+ # recommends using +SCRAM-*+ or +PLAIN+ protected by TLS instead.
14
+ #
15
+ # Additionally, RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use
16
+ # of cleartext and recommends TLS version 1.2 or greater be used for all
17
+ # traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
18
+ class Net::IMAP::CramMD5Authenticator
19
+ def process(challenge)
20
+ digest = hmac_md5(challenge, @password)
21
+ return @user + " " + digest
22
+ end
23
+
24
+ private
25
+
26
+ def initialize(user, password)
27
+ @user = user
28
+ @password = password
29
+ end
30
+
31
+ def hmac_md5(text, key)
32
+ if key.length > 64
33
+ key = Digest::MD5.digest(key)
34
+ end
35
+
36
+ k_ipad = key + "\0" * (64 - key.length)
37
+ k_opad = key + "\0" * (64 - key.length)
38
+ for i in 0..63
39
+ k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
40
+ k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
41
+ end
42
+
43
+ digest = Digest::MD5.digest(k_ipad + text)
44
+
45
+ return Digest::MD5.hexdigest(k_opad + digest)
46
+ end
47
+
48
+ Net::IMAP.add_authenticator "PLAIN", self
49
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+ require "strscan"
5
+
6
+ # Net::IMAP authenticator for the "`DIGEST-MD5`" SASL mechanism type, specified
7
+ # in RFC2831(https://tools.ietf.org/html/rfc2831). See Net::IMAP#authenticate.
8
+ #
9
+ # == Deprecated
10
+ #
11
+ # "+DIGEST-MD5+" has been deprecated by
12
+ # {RFC6331}[https://tools.ietf.org/html/rfc6331] and should not be relied on for
13
+ # security. It is included for compatibility with existing servers.
14
+ class Net::IMAP::DigestMD5Authenticator
15
+ def process(challenge)
16
+ case @stage
17
+ when STAGE_ONE
18
+ @stage = STAGE_TWO
19
+ sparams = {}
20
+ c = StringScanner.new(challenge)
21
+ while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
22
+ k, v = c[1], c[2]
23
+ if v =~ /^"(.*)"$/
24
+ v = $1
25
+ if v =~ /,/
26
+ v = v.split(',')
27
+ end
28
+ end
29
+ sparams[k] = v
30
+ end
31
+
32
+ raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
33
+ raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
34
+
35
+ response = {
36
+ :nonce => sparams['nonce'],
37
+ :username => @user,
38
+ :realm => sparams['realm'],
39
+ :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
40
+ :'digest-uri' => 'imap/' + sparams['realm'],
41
+ :qop => 'auth',
42
+ :maxbuf => 65535,
43
+ :nc => "%08d" % nc(sparams['nonce']),
44
+ :charset => sparams['charset'],
45
+ }
46
+
47
+ response[:authzid] = @authname unless @authname.nil?
48
+
49
+ # now, the real thing
50
+ a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
51
+
52
+ a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
53
+ a1 << ':' + response[:authzid] unless response[:authzid].nil?
54
+
55
+ a2 = "AUTHENTICATE:" + response[:'digest-uri']
56
+ a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
57
+
58
+ response[:response] = Digest::MD5.hexdigest(
59
+ [
60
+ Digest::MD5.hexdigest(a1),
61
+ response.values_at(:nonce, :nc, :cnonce, :qop),
62
+ Digest::MD5.hexdigest(a2)
63
+ ].join(':')
64
+ )
65
+
66
+ return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
67
+ when STAGE_TWO
68
+ @stage = nil
69
+ # if at the second stage, return an empty string
70
+ if challenge =~ /rspauth=/
71
+ return ''
72
+ else
73
+ raise ResponseParseError, challenge
74
+ end
75
+ else
76
+ raise ResponseParseError, challenge
77
+ end
78
+ end
79
+
80
+ def initialize(user, password, authname = nil)
81
+ @user, @password, @authname = user, password, authname
82
+ @nc, @stage = {}, STAGE_ONE
83
+ end
84
+
85
+ private
86
+
87
+ STAGE_ONE = :stage_one
88
+ STAGE_TWO = :stage_two
89
+
90
+ def nc(nonce)
91
+ if @nc.has_key? nonce
92
+ @nc[nonce] = @nc[nonce] + 1
93
+ else
94
+ @nc[nonce] = 1
95
+ end
96
+ return @nc[nonce]
97
+ end
98
+
99
+ # some responses need quoting
100
+ def qdval(k, v)
101
+ return if k.nil? or v.nil?
102
+ if %w"username authzid realm nonce cnonce digest-uri qop".include? k
103
+ v.gsub!(/([\\"])/, "\\\1")
104
+ return '%s="%s"' % [k, v]
105
+ else
106
+ return '%s=%s' % [k, v]
107
+ end
108
+ end
109
+
110
+ Net::IMAP.add_authenticator "DIGEST-MD5", self
111
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authenticator for the "+LOGIN+" SASL mechanism. See Net::IMAP#authenticate.
4
+ #
5
+ # +LOGIN+ authentication sends the password in cleartext.
6
+ # RFC3501[https://tools.ietf.org/html/rfc3501] encourages servers to disable
7
+ # cleartext authentication until after TLS has been negotiated.
8
+ # RFC8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
9
+ # greater be used for all traffic, and deprecate cleartext access ASAP. +LOGIN+
10
+ # can be secured by TLS encryption.
11
+ #
12
+ # == Deprecated
13
+ #
14
+ # The {SASL mechanisms
15
+ # registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
16
+ # marks "LOGIN" as obsoleted in favor of "PLAIN". It is included here for
17
+ # compatibility with existing servers. See
18
+ # {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login]
19
+ # for both specification and deprecation.
20
+ class Net::IMAP::LoginAuthenticator
21
+ def process(data)
22
+ case @state
23
+ when STATE_USER
24
+ @state = STATE_PASSWORD
25
+ return @user
26
+ when STATE_PASSWORD
27
+ return @password
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ STATE_USER = :USER
34
+ STATE_PASSWORD = :PASSWORD
35
+
36
+ def initialize(user, password)
37
+ @user = user
38
+ @password = password
39
+ @state = STATE_USER
40
+ end
41
+
42
+ Net::IMAP.add_authenticator "LOGIN", self
43
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authenticator for the "+PLAIN+" SASL mechanism, specified in
4
+ # RFC4616[https://tools.ietf.org/html/rfc4616]. See Net::IMAP#authenticate.
5
+ #
6
+ # +PLAIN+ authentication sends the password in cleartext.
7
+ # RFC3501[https://tools.ietf.org/html/rfc3501] encourages servers to disable
8
+ # cleartext authentication until after TLS has been negotiated.
9
+ # RFC8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
10
+ # greater be used for all traffic, and deprecate cleartext access ASAP. +PLAIN+
11
+ # can be secured by TLS encryption.
12
+ class Net::IMAP::PlainAuthenticator
13
+
14
+ def process(data)
15
+ return "#@authzid\0#@username\0#@password"
16
+ end
17
+
18
+ # :nodoc:
19
+ NULL = -"\0".b
20
+
21
+ private
22
+
23
+ # +username+ is the authentication identity, the identity whose +password+ is
24
+ # used. +username+ is referred to as +authcid+ by
25
+ # RFC4616[https://tools.ietf.org/html/rfc4616].
26
+ #
27
+ # +authzid+ is the authorization identity (identity to act as). It can
28
+ # usually be left blank. When +authzid+ is left blank (nil or empty string)
29
+ # the server will derive an identity from the credentials and use that as the
30
+ # authorization identity.
31
+ def initialize(username, password, authzid: nil)
32
+ raise ArgumentError, "username contains NULL" if username&.include?(NULL)
33
+ raise ArgumentError, "password contains NULL" if password&.include?(NULL)
34
+ raise ArgumentError, "authzid contains NULL" if authzid&.include?(NULL)
35
+ @username = username
36
+ @password = password
37
+ @authzid = authzid
38
+ end
39
+
40
+ Net::IMAP.add_authenticator "PLAIN", self
41
+ end
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP < Protocol
5
+
6
+ private
7
+
8
+ def validate_data(data)
9
+ case data
10
+ when nil
11
+ when String
12
+ when Integer
13
+ NumValidator.ensure_number(data)
14
+ when Array
15
+ if data[0] == 'CHANGEDSINCE'
16
+ NumValidator.ensure_mod_sequence_value(data[1])
17
+ else
18
+ data.each do |i|
19
+ validate_data(i)
20
+ end
21
+ end
22
+ when Time
23
+ when Symbol
24
+ else
25
+ data.validate
26
+ end
27
+ end
28
+
29
+ def send_data(data, tag = nil)
30
+ case data
31
+ when nil
32
+ put_string("NIL")
33
+ when String
34
+ send_string_data(data, tag)
35
+ when Integer
36
+ send_number_data(data)
37
+ when Array
38
+ send_list_data(data, tag)
39
+ when Time
40
+ send_time_data(data)
41
+ when Symbol
42
+ send_symbol_data(data)
43
+ else
44
+ data.send_data(self, tag)
45
+ end
46
+ end
47
+
48
+ def send_string_data(str, tag = nil)
49
+ case str
50
+ when ""
51
+ put_string('""')
52
+ when /[\x80-\xff\r\n]/n
53
+ # literal
54
+ send_literal(str, tag)
55
+ when /[(){ \x00-\x1f\x7f%*"\\]/n
56
+ # quoted string
57
+ send_quoted_string(str)
58
+ else
59
+ put_string(str)
60
+ end
61
+ end
62
+
63
+ def send_quoted_string(str)
64
+ put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
65
+ end
66
+
67
+ def send_literal(str, tag = nil)
68
+ synchronize do
69
+ put_string("{" + str.bytesize.to_s + "}" + CRLF)
70
+ @continued_command_tag = tag
71
+ @continuation_request_exception = nil
72
+ begin
73
+ @continuation_request_arrival.wait
74
+ e = @continuation_request_exception || @exception
75
+ raise e if e
76
+ put_string(str)
77
+ ensure
78
+ @continued_command_tag = nil
79
+ @continuation_request_exception = nil
80
+ end
81
+ end
82
+ end
83
+
84
+ def send_number_data(num)
85
+ put_string(num.to_s)
86
+ end
87
+
88
+ def send_list_data(list, tag = nil)
89
+ put_string("(")
90
+ first = true
91
+ list.each do |i|
92
+ if first
93
+ first = false
94
+ else
95
+ put_string(" ")
96
+ end
97
+ send_data(i, tag)
98
+ end
99
+ put_string(")")
100
+ end
101
+
102
+ DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
103
+
104
+ def send_time_data(time)
105
+ t = time.dup.gmtime
106
+ s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
107
+ t.day, DATE_MONTH[t.month - 1], t.year,
108
+ t.hour, t.min, t.sec)
109
+ put_string(s)
110
+ end
111
+
112
+ def send_symbol_data(symbol)
113
+ put_string("\\" + symbol.to_s)
114
+ end
115
+
116
+ class RawData # :nodoc:
117
+ def send_data(imap, tag)
118
+ imap.__send__(:put_string, @data)
119
+ end
120
+
121
+ def validate
122
+ end
123
+
124
+ private
125
+
126
+ def initialize(data)
127
+ @data = data
128
+ end
129
+ end
130
+
131
+ class Atom # :nodoc:
132
+ def send_data(imap, tag)
133
+ imap.__send__(:put_string, @data)
134
+ end
135
+
136
+ def validate
137
+ end
138
+
139
+ private
140
+
141
+ def initialize(data)
142
+ @data = data
143
+ end
144
+ end
145
+
146
+ class QuotedString # :nodoc:
147
+ def send_data(imap, tag)
148
+ imap.__send__(:send_quoted_string, @data)
149
+ end
150
+
151
+ def validate
152
+ end
153
+
154
+ private
155
+
156
+ def initialize(data)
157
+ @data = data
158
+ end
159
+ end
160
+
161
+ class Literal # :nodoc:
162
+ def send_data(imap, tag)
163
+ imap.__send__(:send_literal, @data, tag)
164
+ end
165
+
166
+ def validate
167
+ end
168
+
169
+ private
170
+
171
+ def initialize(data)
172
+ @data = data
173
+ end
174
+ end
175
+
176
+ class MessageSet # :nodoc:
177
+ def send_data(imap, tag)
178
+ imap.__send__(:put_string, format_internal(@data))
179
+ end
180
+
181
+ def validate
182
+ validate_internal(@data)
183
+ end
184
+
185
+ private
186
+
187
+ def initialize(data)
188
+ @data = data
189
+ end
190
+
191
+ def format_internal(data)
192
+ case data
193
+ when "*"
194
+ return data
195
+ when Integer
196
+ if data == -1
197
+ return "*"
198
+ else
199
+ return data.to_s
200
+ end
201
+ when Range
202
+ return format_internal(data.first) +
203
+ ":" + format_internal(data.last)
204
+ when Array
205
+ return data.collect {|i| format_internal(i)}.join(",")
206
+ when ThreadMember
207
+ return data.seqno.to_s +
208
+ ":" + data.children.collect {|i| format_internal(i).join(",")}
209
+ end
210
+ end
211
+
212
+ def validate_internal(data)
213
+ case data
214
+ when "*"
215
+ when Integer
216
+ NumValidator.ensure_nz_number(data)
217
+ when Range
218
+ when Array
219
+ data.each do |i|
220
+ validate_internal(i)
221
+ end
222
+ when ThreadMember
223
+ data.children.each do |i|
224
+ validate_internal(i)
225
+ end
226
+ else
227
+ raise DataFormatError, data.inspect
228
+ end
229
+ end
230
+ end
231
+
232
+ class ClientID # :nodoc:
233
+
234
+ def send_data(imap, tag)
235
+ imap.__send__(:send_data, format_internal(@data), tag)
236
+ end
237
+
238
+ def validate
239
+ validate_internal(@data)
240
+ end
241
+
242
+ private
243
+
244
+ def initialize(data)
245
+ @data = data
246
+ end
247
+
248
+ def validate_internal(client_id)
249
+ client_id.to_h.each do |k,v|
250
+ unless StringFormatter.valid_string?(k)
251
+ raise DataFormatError, client_id.inspect
252
+ end
253
+ end
254
+ rescue NoMethodError, TypeError # to_h failed
255
+ raise DataFormatError, client_id.inspect
256
+ end
257
+
258
+ def format_internal(client_id)
259
+ return nil if client_id.nil?
260
+ client_id.to_h.flat_map {|k,v|
261
+ [StringFormatter.string(k), StringFormatter.nstring(v)]
262
+ }
263
+ end
264
+
265
+ end
266
+
267
+ module StringFormatter
268
+
269
+ LITERAL_REGEX = /[\x80-\xff\r\n]/n
270
+
271
+ module_function
272
+
273
+ # Allows symbols in addition to strings
274
+ def valid_string?(str)
275
+ str.is_a?(Symbol) || str.respond_to?(:to_str)
276
+ end
277
+
278
+ # Allows nil, symbols, and strings
279
+ def valid_nstring?(str)
280
+ str.nil? || valid_string?(str)
281
+ end
282
+
283
+ # coerces using +to_s+
284
+ def string(str)
285
+ str = str.to_s
286
+ if str =~ LITERAL_REGEX
287
+ Literal.new(str)
288
+ else
289
+ QuotedString.new(str)
290
+ end
291
+ end
292
+
293
+ # coerces non-nil using +to_s+
294
+ def nstring(str)
295
+ str.nil? ? nil : string(str)
296
+ end
297
+
298
+ end
299
+
300
+ end
301
+ end