net-imap 0.1.1 → 0.2.3

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