net-imap 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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