net-imap 0.3.7 → 0.4.9

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 (57) 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 +2 -0
  5. data/Gemfile +3 -0
  6. data/README.md +15 -4
  7. data/Rakefile +0 -7
  8. data/docs/styles.css +0 -12
  9. data/lib/net/imap/authenticators.rb +26 -57
  10. data/lib/net/imap/command_data.rb +13 -6
  11. data/lib/net/imap/data_encoding.rb +14 -2
  12. data/lib/net/imap/deprecated_client_options.rb +139 -0
  13. data/lib/net/imap/errors.rb +20 -0
  14. data/lib/net/imap/fetch_data.rb +518 -0
  15. data/lib/net/imap/response_data.rb +178 -255
  16. data/lib/net/imap/response_parser/parser_utils.rb +240 -0
  17. data/lib/net/imap/response_parser.rb +1722 -1193
  18. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  19. data/lib/net/imap/sasl/authentication_exchange.rb +107 -0
  20. data/lib/net/imap/sasl/authenticators.rb +118 -0
  21. data/lib/net/imap/sasl/client_adapter.rb +72 -0
  22. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +21 -11
  23. data/lib/net/imap/sasl/digest_md5_authenticator.rb +180 -0
  24. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  25. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  26. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +25 -16
  27. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  28. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  29. data/lib/net/imap/sasl/protocol_adapters.rb +45 -0
  30. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  31. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  32. data/lib/net/imap/sasl/stringprep.rb +6 -66
  33. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  34. data/lib/net/imap/sasl.rb +144 -43
  35. data/lib/net/imap/sasl_adapter.rb +21 -0
  36. data/lib/net/imap/search_result.rb +150 -0
  37. data/lib/net/imap/sequence_set.rb +1414 -0
  38. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  39. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  40. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  41. data/lib/net/imap/stringprep/tables.rb +146 -0
  42. data/lib/net/imap/stringprep/trace.rb +85 -0
  43. data/lib/net/imap/stringprep.rb +159 -0
  44. data/lib/net/imap.rb +1213 -636
  45. data/net-imap.gemspec +5 -3
  46. data/rakelib/benchmarks.rake +91 -0
  47. data/rakelib/saslprep.rake +4 -4
  48. data/rakelib/string_prep_tables_generator.rb +82 -60
  49. metadata +34 -14
  50. data/benchmarks/stringprep.yml +0 -65
  51. data/benchmarks/table-regexps.yml +0 -39
  52. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  53. data/lib/net/imap/authenticators/plain.rb +0 -41
  54. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  55. data/lib/net/imap/sasl/saslprep.rb +0 -55
  56. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  57. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
data/lib/net/imap/sasl.rb CHANGED
@@ -6,12 +6,11 @@ module Net
6
6
  # Pluggable authentication mechanisms for protocols which support SASL
7
7
  # (Simple Authentication and Security Layer), such as IMAP4, SMTP, LDAP, and
8
8
  # XMPP. {RFC-4422}[https://tools.ietf.org/html/rfc4422] specifies the
9
- # common SASL framework and the +EXTERNAL+ mechanism, and the
10
- # {SASL mechanism registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
11
- # lists the specification for others.
12
- #
13
- # "SASL is conceptually a framework that provides an abstraction layer
14
- # between protocols and mechanisms as illustrated in the following diagram."
9
+ # common \SASL framework:
10
+ # >>>
11
+ # SASL is conceptually a framework that provides an abstraction layer
12
+ # between protocols and mechanisms as illustrated in the following
13
+ # diagram.
15
14
  #
16
15
  # SMTP LDAP XMPP Other protocols ...
17
16
  # \ | | /
@@ -21,58 +20,160 @@ module Net
21
20
  # / | | \
22
21
  # EXTERNAL GSSAPI PLAIN Other mechanisms ...
23
22
  #
23
+ # Net::IMAP uses SASL via the Net::IMAP#authenticate method.
24
+ #
25
+ # == Mechanisms
26
+ #
27
+ # Each mechanism has different properties and requirements. Please consult
28
+ # the documentation for the specific mechanisms you are using:
29
+ #
30
+ # +ANONYMOUS+::
31
+ # See AnonymousAuthenticator.
32
+ #
33
+ # Allows the user to gain access to public services or resources without
34
+ # authenticating or disclosing an identity.
35
+ #
36
+ # +EXTERNAL+::
37
+ # See ExternalAuthenticator.
38
+ #
39
+ # Authenticates using already established credentials, such as a TLS
40
+ # certificate or IPsec.
41
+ #
42
+ # +OAUTHBEARER+::
43
+ # See OAuthBearerAuthenticator.
44
+ #
45
+ # Login using an OAuth2 Bearer token. This is the standard mechanism
46
+ # for using OAuth2 with \SASL, but it is not yet deployed as widely as
47
+ # +XOAUTH2+.
48
+ #
49
+ # +PLAIN+::
50
+ # See PlainAuthenticator.
51
+ #
52
+ # Login using clear-text username and password.
53
+ #
54
+ # +SCRAM-SHA-1+::
55
+ # +SCRAM-SHA-256+::
56
+ # See ScramAuthenticator.
57
+ #
58
+ # Login by username and password. The password is not sent to the
59
+ # server but is used in a salted challenge/response exchange.
60
+ # +SCRAM-SHA-1+ and +SCRAM-SHA-256+ are directly supported by
61
+ # Net::IMAP::SASL. New authenticators can easily be added for any other
62
+ # <tt>SCRAM-*</tt> mechanism if the digest algorithm is supported by
63
+ # OpenSSL::Digest.
64
+ #
65
+ # +XOAUTH2+::
66
+ # See XOAuth2Authenticator.
67
+ #
68
+ # Login using a username and an OAuth2 access token. Non-standard and
69
+ # obsoleted by +OAUTHBEARER+, but widely supported.
70
+ #
71
+ # See the {SASL mechanism
72
+ # registry}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
73
+ # for a list of all SASL mechanisms and their specifications. To register
74
+ # new authenticators, see Authenticators.
75
+ #
76
+ # === Deprecated mechanisms
77
+ #
78
+ # <em>Obsolete mechanisms should be avoided, but are still available for
79
+ # backwards compatibility.</em>
80
+ #
81
+ # >>>
82
+ # For +DIGEST-MD5+ see DigestMD5Authenticator.
83
+ #
84
+ # For +LOGIN+, see LoginAuthenticator.
85
+ #
86
+ # For +CRAM-MD5+, see CramMD5Authenticator.
87
+ #
88
+ # <em>Using a deprecated mechanism will print a warning.</em>
89
+ #
24
90
  module SASL
91
+ # Exception class for any client error detected during the authentication
92
+ # exchange.
93
+ #
94
+ # When the _server_ reports an authentication failure, it will respond
95
+ # with a protocol specific error instead, e.g: +BAD+ or +NO+ in IMAP.
96
+ #
97
+ # When the client encounters any error, it *must* consider the
98
+ # authentication exchange to be unsuccessful and it might need to drop the
99
+ # connection. For example, if the server reports that the authentication
100
+ # exchange was successful or the protocol does not allow additional
101
+ # authentication attempts.
102
+ Error = Class.new(StandardError)
25
103
 
26
- # autoloading to avoid loading all of the regexps when they aren't used.
104
+ # Indicates an authentication exchange that will be or has been canceled
105
+ # by the client, not due to any error or failure during processing.
106
+ AuthenticationCanceled = Class.new(Error)
27
107
 
28
- autoload :StringPrep, File.expand_path("sasl/stringprep", __dir__)
29
- autoload :SASLprep, File.expand_path("#{__dir__}/sasl/saslprep", __dir__)
108
+ # Indicates an error when processing a server challenge, e.g: an invalid
109
+ # or unparsable challenge. An underlying exception may be available as
110
+ # the exception's #cause.
111
+ AuthenticationError = Class.new(Error)
30
112
 
31
- # ArgumentError raised when +string+ is invalid for the stringprep
32
- # +profile+.
33
- class StringPrepError < ArgumentError
34
- attr_reader :string, :profile
113
+ # Indicates that authentication cannot proceed because one of the server's
114
+ # messages has not passed integrity checks.
115
+ AuthenticationFailed = Class.new(Error)
35
116
 
36
- def initialize(*args, string: nil, profile: nil)
37
- @string = -string.to_str unless string.nil?
38
- @profile = -profile.to_str unless profile.nil?
39
- super(*args)
40
- end
41
- end
117
+ # Indicates that authentication cannot proceed because one of the server's
118
+ # ended authentication prematurely.
119
+ class AuthenticationIncomplete < AuthenticationFailed
120
+ # The success response from the server
121
+ attr_reader :response
42
122
 
43
- # StringPrepError raised when +string+ contains a codepoint prohibited by
44
- # +table+.
45
- class ProhibitedCodepoint < StringPrepError
46
- attr_reader :table
47
-
48
- def initialize(table, *args, **kwargs)
49
- @table = -table.to_str
50
- details = (title = StringPrep::TABLE_TITLES[table]) ?
51
- "%s [%s]" % [title, table] : table
52
- message = "String contains a prohibited codepoint: %s" % [details]
53
- super(message, *args, **kwargs)
123
+ def initialize(response, message = "authentication ended prematurely")
124
+ super(message)
125
+ @response = response
54
126
  end
55
127
  end
56
128
 
57
- # StringPrepError raised when +string+ contains bidirectional characters
58
- # which violate the StringPrep requirements.
59
- class BidiStringError < StringPrepError
129
+ # autoloading to avoid loading all of the regexps when they aren't used.
130
+ sasl_stringprep_rb = File.expand_path("sasl/stringprep", __dir__)
131
+ autoload :StringPrep, sasl_stringprep_rb
132
+ autoload :SASLprep, sasl_stringprep_rb
133
+ autoload :StringPrepError, sasl_stringprep_rb
134
+ autoload :ProhibitedCodepoint, sasl_stringprep_rb
135
+ autoload :BidiStringError, sasl_stringprep_rb
136
+
137
+ sasl_dir = File.expand_path("sasl", __dir__)
138
+ autoload :AuthenticationExchange, "#{sasl_dir}/authentication_exchange"
139
+ autoload :ClientAdapter, "#{sasl_dir}/client_adapter"
140
+ autoload :ProtocolAdapters, "#{sasl_dir}/protocol_adapters"
141
+
142
+ autoload :Authenticators, "#{sasl_dir}/authenticators"
143
+ autoload :GS2Header, "#{sasl_dir}/gs2_header"
144
+ autoload :ScramAlgorithm, "#{sasl_dir}/scram_algorithm"
145
+
146
+ autoload :AnonymousAuthenticator, "#{sasl_dir}/anonymous_authenticator"
147
+ autoload :ExternalAuthenticator, "#{sasl_dir}/external_authenticator"
148
+ autoload :OAuthBearerAuthenticator, "#{sasl_dir}/oauthbearer_authenticator"
149
+ autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
150
+ autoload :ScramAuthenticator, "#{sasl_dir}/scram_authenticator"
151
+ autoload :ScramSHA1Authenticator, "#{sasl_dir}/scram_authenticator"
152
+ autoload :ScramSHA256Authenticator, "#{sasl_dir}/scram_authenticator"
153
+ autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
154
+
155
+ autoload :CramMD5Authenticator, "#{sasl_dir}/cram_md5_authenticator"
156
+ autoload :DigestMD5Authenticator, "#{sasl_dir}/digest_md5_authenticator"
157
+ autoload :LoginAuthenticator, "#{sasl_dir}/login_authenticator"
158
+
159
+ # Returns the default global SASL::Authenticators instance.
160
+ def self.authenticators; @authenticators ||= Authenticators.new end
161
+
162
+ # Delegates to <tt>registry.new</tt> See Authenticators#new.
163
+ def self.authenticator(*args, registry: authenticators, **kwargs, &block)
164
+ registry.new(*args, **kwargs, &block)
60
165
  end
61
166
 
62
- #--
63
- # We could just extend SASLprep module directly. It's done this way so
64
- # SASLprep can be lazily autoloaded. Most users won't need it.
65
- #++
66
- extend self
167
+ # Delegates to ::authenticators. See Authenticators#add_authenticator.
168
+ def self.add_authenticator(...) authenticators.add_authenticator(...) end
169
+
170
+ module_function
67
171
 
68
- # See SASLprep#saslprep.
172
+ # See Net::IMAP::StringPrep::SASLprep#saslprep.
69
173
  def saslprep(string, **opts)
70
- SASLprep.saslprep(string, **opts)
174
+ Net::IMAP::StringPrep::SASLprep.saslprep(string, **opts)
71
175
  end
72
176
 
73
177
  end
74
178
  end
75
-
76
179
  end
77
-
78
- Net::IMAP.extend Net::IMAP::SASL
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+
6
+ # Experimental
7
+ class SASLAdapter < SASL::ClientAdapter
8
+ include SASL::ProtocolAdapters::IMAP
9
+
10
+ RESPONSE_ERRORS = [NoResponseError, BadResponseError, ByeResponseError]
11
+ .freeze
12
+
13
+ def response_errors; RESPONSE_ERRORS end
14
+ def sasl_ir_capable?; client.capable?("SASL-IR") end
15
+ def auth_capable?(mechanism); client.auth_capable?(mechanism) end
16
+ def drop_connection; client.logout! end
17
+ def drop_connection!; client.disconnect end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+
6
+ # An array of sequence numbers returned by Net::IMAP#search, or unique
7
+ # identifiers returned by Net::IMAP#uid_search.
8
+ #
9
+ # For backward compatibility, SearchResult inherits from Array.
10
+ class SearchResult < Array
11
+
12
+ # Returns a frozen SearchResult populated with the given +seq_nums+.
13
+ #
14
+ # Net::IMAP::SearchResult[1, 3, 5, modseq: 9]
15
+ # # => Net::IMAP::SearchResult[1, 3, 5, modseq: 9]
16
+ def self.[](*seq_nums, modseq: nil)
17
+ new(seq_nums, modseq: modseq)
18
+ end
19
+
20
+ # A modification sequence number, as described by the +CONDSTORE+
21
+ # extension in {[RFC7162
22
+ # §3.1.6]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1.6].
23
+ attr_reader :modseq
24
+
25
+ # Returns a frozen SearchResult populated with the given +seq_nums+.
26
+ #
27
+ # Net::IMAP::SearchResult.new([1, 3, 5], modseq: 9)
28
+ # # => Net::IMAP::SearchResult[1, 3, 5, modseq: 9]
29
+ def initialize(seq_nums, modseq: nil)
30
+ super(seq_nums.to_ary.map { Integer _1 })
31
+ @modseq = Integer modseq if modseq
32
+ freeze
33
+ end
34
+
35
+ # Returns a frozen copy of +other+.
36
+ def initialize_copy(other); super; freeze end
37
+
38
+ # Returns whether +other+ is a SearchResult with the same values and the
39
+ # same #modseq. The order of numbers is irrelevant.
40
+ #
41
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
42
+ # Net::IMAP::SearchResult[123, 456, modseq: 789]
43
+ # # => true
44
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
45
+ # Net::IMAP::SearchResult[456, 123, modseq: 789]
46
+ # # => true
47
+ #
48
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
49
+ # Net::IMAP::SearchResult[987, 654, modseq: 789]
50
+ # # => false
51
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
52
+ # Net::IMAP::SearchResult[1, 2, 3, modseq: 9999]
53
+ # # => false
54
+ #
55
+ # SearchResult can be compared directly with Array, if #modseq is nil and
56
+ # the array is sorted.
57
+ #
58
+ # Net::IMAP::SearchResult[9, 8, 6, 4, 1] == [1, 4, 6, 8, 9] # => true
59
+ # Net::IMAP::SearchResult[3, 5, 7, modseq: 99] == [3, 5, 7] # => false
60
+ #
61
+ # Note that Array#== does require matching order and ignores #modseq.
62
+ #
63
+ # [9, 8, 6, 4, 1] == Net::IMAP::SearchResult[1, 4, 6, 8, 9] # => false
64
+ # [3, 5, 7] == Net::IMAP::SearchResult[3, 5, 7, modseq: 99] # => true
65
+ #
66
+ def ==(other)
67
+ (modseq ?
68
+ other.is_a?(self.class) && modseq == other.modseq :
69
+ other.is_a?(Array)) &&
70
+ size == other.size &&
71
+ sort == other.sort
72
+ end
73
+
74
+ # Hash equality. Unlike #==, order will be taken into account.
75
+ def hash
76
+ return super if modseq.nil?
77
+ [super, self.class, modseq].hash
78
+ end
79
+
80
+ # Hash equality. Unlike #==, order will be taken into account.
81
+ def eql?(other)
82
+ return super if modseq.nil?
83
+ self.class == other.class && hash == other.hash
84
+ end
85
+
86
+ # Returns a string that represents the SearchResult.
87
+ #
88
+ # Net::IMAP::SearchResult[123, 456, 789].inspect
89
+ # # => "[123, 456, 789]"
90
+ #
91
+ # Net::IMAP::SearchResult[543, 210, 678, modseq: 2048].inspect
92
+ # # => "Net::IMAP::SearchResult[543, 210, 678, modseq: 2048]"
93
+ #
94
+ def inspect
95
+ return super if modseq.nil?
96
+ "%s[%s, modseq: %p]" % [self.class, join(", "), modseq]
97
+ end
98
+
99
+ # Returns a string that follows the formal \IMAP syntax.
100
+ #
101
+ # data = Net::IMAP::SearchResult[2, 8, 32, 128, 256, 512]
102
+ # data.to_s # => "* SEARCH 2 8 32 128 256 512"
103
+ # data.to_s("SEARCH") # => "* SEARCH 2 8 32 128 256 512"
104
+ # data.to_s("SORT") # => "* SORT 2 8 32 128 256 512"
105
+ # data.to_s(nil) # => "2 8 32 128 256 512"
106
+ #
107
+ # data = Net::IMAP::SearchResult[1, 3, 16, 1024, modseq: 2048].to_s
108
+ # data.to_s # => "* SEARCH 1 3 16 1024 (MODSEQ 2048)"
109
+ # data.to_s("SORT") # => "* SORT 1 3 16 1024 (MODSEQ 2048)"
110
+ # data.to_s # => "1 3 16 1024 (MODSEQ 2048)"
111
+ #
112
+ def to_s(type = "SEARCH")
113
+ str = +""
114
+ str << "* %s " % [type.to_str] unless type.nil?
115
+ str << join(" ")
116
+ str << " (MODSEQ %d)" % [modseq] if modseq
117
+ -str
118
+ end
119
+
120
+ # Converts the SearchResult into a SequenceSet.
121
+ #
122
+ # Net::IMAP::SearchResult[9, 1, 2, 4, 10, 12, 3, modseq: 123_456]
123
+ # .to_sequence_set
124
+ # # => Net::IMAP::SequenceSet["1:4,9:10,12"]
125
+ def to_sequence_set; SequenceSet[*self] end
126
+
127
+ def pretty_print(pp)
128
+ return super if modseq.nil?
129
+ pp.text self.class.name + "["
130
+ pp.group_sub do
131
+ pp.nest(2) do
132
+ pp.breakable ""
133
+ each do |num|
134
+ pp.pp num
135
+ pp.text ","
136
+ pp.fill_breakable
137
+ end
138
+ pp.breakable ""
139
+ pp.text "modseq: "
140
+ pp.pp modseq
141
+ end
142
+ pp.breakable ""
143
+ pp.text "]"
144
+ end
145
+ end
146
+
147
+ end
148
+
149
+ end
150
+ end