net-imap 0.1.1 → 0.2.3

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3963dc001573cfafeb31305d2ba569c89bf14ec1fd1b75afa5b27d3d8f94ee8
4
- data.tar.gz: 0feb88b9ed0be0c63684f291060321f493e43343e5b761ef0019977b1c7cc3b7
3
+ metadata.gz: ee45560f32705f69d591b21df3d54372e69b444e3cb40f540f44b299c24a1803
4
+ data.tar.gz: 2aa318c6367dee1ce530139e06c9b78b0cf7e8eeb9411ef873f290dda78b6273
5
5
  SHA512:
6
- metadata.gz: 2a0a7efa65c793e3022b7768669a69b821894bee3610f92c27a95626407ac6ce194ddb1e32f395dc9bad3e68b91f94d54f47a823fedffd13c7b5cea2cd4ba50d
7
- data.tar.gz: 1b1b1ae36f4be343cddf551e4c86cda1848406a04edf70238fd8efd3aa793fad7b180b782b9044b44235d202866b50de4efb26fdac2da01bd3d27f3ef3ae278f
6
+ metadata.gz: f98f22799e9e1bff9c8f191d510688f52b7a7737de7fce8b76e42da1cfe8672a94cbb6c3a1eb24d7fce3d1855e6d809dfda52d91f2a67a4d216f66ba015960a9
7
+ data.tar.gz: d51b6eb6901db8742ed404714cdd0f54c46cf965cbf2de0130cea863f41a84e0779aac631689c7b0913f77e0fecd2184a66054fc0699bde5d4825db7fb188829
@@ -7,18 +7,25 @@ jobs:
7
7
  name: build (${{ matrix.ruby }} / ${{ matrix.os }})
8
8
  strategy:
9
9
  matrix:
10
- ruby: [ 2.7, 2.6, 2.5, head ]
10
+ ruby: [ head, '3.0', '2.7' ]
11
11
  os: [ ubuntu-latest, macos-latest ]
12
+ experimental: [false]
13
+ include:
14
+ # - ruby: 2.6
15
+ # os: ubuntu-latest
16
+ # experimental: true
17
+ - ruby: 2.6
18
+ os: macos-latest
19
+ experimental: false
12
20
  runs-on: ${{ matrix.os }}
21
+ continue-on-error: ${{ matrix.experimental }}
13
22
  steps:
14
- - uses: actions/checkout@master
23
+ - uses: actions/checkout@v2
15
24
  - name: Set up Ruby
16
25
  uses: ruby/setup-ruby@v1
17
26
  with:
18
27
  ruby-version: ${{ matrix.ruby }}
19
28
  - name: Install dependencies
20
- run: |
21
- gem install bundler --no-document
22
- bundle install
29
+ run: bundle install
23
30
  - name: Run test
24
31
  run: rake test
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ /Gemfile.lock
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Net::IMAP
2
2
 
3
3
  Net::IMAP implements Internet Message Access Protocol (IMAP) client
4
- functionality. The protocol is described in [IMAP].
4
+ functionality. The protocol is described in [IMAP](https://tools.ietf.org/html/rfc3501).
5
5
 
6
6
  ## Installation
7
7
 
data/Rakefile CHANGED
@@ -7,4 +7,11 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList["test/**/test_*.rb"]
8
8
  end
9
9
 
10
+ task :sync_tool do
11
+ require 'fileutils'
12
+ FileUtils.cp "../ruby/tool/lib/core_assertions.rb", "./test/lib"
13
+ FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib"
14
+ FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib"
15
+ end
16
+
10
17
  task :default => :test
@@ -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 "CRAM-MD5", 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,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,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Net
6
+ class IMAP < Protocol
7
+
8
+ private
9
+
10
+ def validate_data(data)
11
+ case data
12
+ when nil
13
+ when String
14
+ when Integer
15
+ NumValidator.ensure_number(data)
16
+ when Array
17
+ if data[0] == 'CHANGEDSINCE'
18
+ NumValidator.ensure_mod_sequence_value(data[1])
19
+ else
20
+ data.each do |i|
21
+ validate_data(i)
22
+ end
23
+ end
24
+ when Time
25
+ when Symbol
26
+ else
27
+ data.validate
28
+ end
29
+ end
30
+
31
+ def send_data(data, tag = nil)
32
+ case data
33
+ when nil
34
+ put_string("NIL")
35
+ when String
36
+ send_string_data(data, tag)
37
+ when Integer
38
+ send_number_data(data)
39
+ when Array
40
+ send_list_data(data, tag)
41
+ when Time
42
+ send_time_data(data)
43
+ when Symbol
44
+ send_symbol_data(data)
45
+ else
46
+ data.send_data(self, tag)
47
+ end
48
+ end
49
+
50
+ def send_string_data(str, tag = nil)
51
+ case str
52
+ when ""
53
+ put_string('""')
54
+ when /[\x80-\xff\r\n]/n
55
+ # literal
56
+ send_literal(str, tag)
57
+ when /[(){ \x00-\x1f\x7f%*"\\]/n
58
+ # quoted string
59
+ send_quoted_string(str)
60
+ else
61
+ put_string(str)
62
+ end
63
+ end
64
+
65
+ def send_quoted_string(str)
66
+ put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
67
+ end
68
+
69
+ def send_literal(str, tag = nil)
70
+ synchronize do
71
+ put_string("{" + str.bytesize.to_s + "}" + CRLF)
72
+ @continued_command_tag = tag
73
+ @continuation_request_exception = nil
74
+ begin
75
+ @continuation_request_arrival.wait
76
+ e = @continuation_request_exception || @exception
77
+ raise e if e
78
+ put_string(str)
79
+ ensure
80
+ @continued_command_tag = nil
81
+ @continuation_request_exception = nil
82
+ end
83
+ end
84
+ end
85
+
86
+ def send_number_data(num)
87
+ put_string(num.to_s)
88
+ end
89
+
90
+ def send_list_data(list, tag = nil)
91
+ put_string("(")
92
+ first = true
93
+ list.each do |i|
94
+ if first
95
+ first = false
96
+ else
97
+ put_string(" ")
98
+ end
99
+ send_data(i, tag)
100
+ end
101
+ put_string(")")
102
+ end
103
+
104
+ DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
105
+
106
+ def send_time_data(time)
107
+ t = time.dup.gmtime
108
+ s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
109
+ t.day, DATE_MONTH[t.month - 1], t.year,
110
+ t.hour, t.min, t.sec)
111
+ put_string(s)
112
+ end
113
+
114
+ def send_symbol_data(symbol)
115
+ put_string("\\" + symbol.to_s)
116
+ end
117
+
118
+ class RawData # :nodoc:
119
+ def send_data(imap, tag)
120
+ imap.__send__(:put_string, @data)
121
+ end
122
+
123
+ def validate
124
+ end
125
+
126
+ private
127
+
128
+ def initialize(data)
129
+ @data = data
130
+ end
131
+ end
132
+
133
+ class Atom # :nodoc:
134
+ def send_data(imap, tag)
135
+ imap.__send__(:put_string, @data)
136
+ end
137
+
138
+ def validate
139
+ end
140
+
141
+ private
142
+
143
+ def initialize(data)
144
+ @data = data
145
+ end
146
+ end
147
+
148
+ class QuotedString # :nodoc:
149
+ def send_data(imap, tag)
150
+ imap.__send__(:send_quoted_string, @data)
151
+ end
152
+
153
+ def validate
154
+ end
155
+
156
+ private
157
+
158
+ def initialize(data)
159
+ @data = data
160
+ end
161
+ end
162
+
163
+ class Literal # :nodoc:
164
+ def send_data(imap, tag)
165
+ imap.__send__(:send_literal, @data, tag)
166
+ end
167
+
168
+ def validate
169
+ end
170
+
171
+ private
172
+
173
+ def initialize(data)
174
+ @data = data
175
+ end
176
+ end
177
+
178
+ class MessageSet # :nodoc:
179
+ def send_data(imap, tag)
180
+ imap.__send__(:put_string, format_internal(@data))
181
+ end
182
+
183
+ def validate
184
+ validate_internal(@data)
185
+ end
186
+
187
+ private
188
+
189
+ def initialize(data)
190
+ @data = data
191
+ end
192
+
193
+ def format_internal(data)
194
+ case data
195
+ when "*"
196
+ return data
197
+ when Integer
198
+ if data == -1
199
+ return "*"
200
+ else
201
+ return data.to_s
202
+ end
203
+ when Range
204
+ return format_internal(data.first) +
205
+ ":" + format_internal(data.last)
206
+ when Array
207
+ return data.collect {|i| format_internal(i)}.join(",")
208
+ when ThreadMember
209
+ return data.seqno.to_s +
210
+ ":" + data.children.collect {|i| format_internal(i).join(",")}
211
+ end
212
+ end
213
+
214
+ def validate_internal(data)
215
+ case data
216
+ when "*"
217
+ when Integer
218
+ NumValidator.ensure_nz_number(data)
219
+ when Range
220
+ when Array
221
+ data.each do |i|
222
+ validate_internal(i)
223
+ end
224
+ when ThreadMember
225
+ data.children.each do |i|
226
+ validate_internal(i)
227
+ end
228
+ else
229
+ raise DataFormatError, data.inspect
230
+ end
231
+ end
232
+ end
233
+
234
+ class ClientID # :nodoc:
235
+
236
+ def send_data(imap, tag)
237
+ imap.__send__(:send_data, format_internal(@data), tag)
238
+ end
239
+
240
+ def validate
241
+ validate_internal(@data)
242
+ end
243
+
244
+ private
245
+
246
+ def initialize(data)
247
+ @data = data
248
+ end
249
+
250
+ def validate_internal(client_id)
251
+ client_id.to_h.each do |k,v|
252
+ unless StringFormatter.valid_string?(k)
253
+ raise DataFormatError, client_id.inspect
254
+ end
255
+ end
256
+ rescue NoMethodError, TypeError # to_h failed
257
+ raise DataFormatError, client_id.inspect
258
+ end
259
+
260
+ def format_internal(client_id)
261
+ return nil if client_id.nil?
262
+ client_id.to_h.flat_map {|k,v|
263
+ [StringFormatter.string(k), StringFormatter.nstring(v)]
264
+ }
265
+ end
266
+
267
+ end
268
+
269
+ module StringFormatter
270
+
271
+ LITERAL_REGEX = /[\x80-\xff\r\n]/n
272
+
273
+ module_function
274
+
275
+ # Allows symbols in addition to strings
276
+ def valid_string?(str)
277
+ str.is_a?(Symbol) || str.respond_to?(:to_str)
278
+ end
279
+
280
+ # Allows nil, symbols, and strings
281
+ def valid_nstring?(str)
282
+ str.nil? || valid_string?(str)
283
+ end
284
+
285
+ # coerces using +to_s+
286
+ def string(str)
287
+ str = str.to_s
288
+ if str =~ LITERAL_REGEX
289
+ Literal.new(str)
290
+ else
291
+ QuotedString.new(str)
292
+ end
293
+ end
294
+
295
+ # coerces non-nil using +to_s+
296
+ def nstring(str)
297
+ str.nil? ? nil : string(str)
298
+ end
299
+
300
+ end
301
+
302
+ end
303
+ end