net-imap 0.3.7 → 0.4.9

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