net-imap 0.3.4 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/BSDL +22 -0
  3. data/COPYING +56 -0
  4. data/Gemfile +14 -0
  5. data/LICENSE.txt +3 -22
  6. data/README.md +25 -8
  7. data/Rakefile +0 -7
  8. data/docs/styles.css +72 -23
  9. data/lib/net/imap/authenticators.rb +26 -57
  10. data/lib/net/imap/command_data.rb +74 -54
  11. data/lib/net/imap/config/attr_accessors.rb +75 -0
  12. data/lib/net/imap/config/attr_inheritance.rb +90 -0
  13. data/lib/net/imap/config/attr_type_coercion.rb +61 -0
  14. data/lib/net/imap/config.rb +470 -0
  15. data/lib/net/imap/data_encoding.rb +21 -9
  16. data/lib/net/imap/data_lite.rb +226 -0
  17. data/lib/net/imap/deprecated_client_options.rb +142 -0
  18. data/lib/net/imap/errors.rb +27 -1
  19. data/lib/net/imap/esearch_result.rb +180 -0
  20. data/lib/net/imap/fetch_data.rb +597 -0
  21. data/lib/net/imap/flags.rb +1 -1
  22. data/lib/net/imap/response_data.rb +250 -440
  23. data/lib/net/imap/response_parser/parser_utils.rb +245 -0
  24. data/lib/net/imap/response_parser.rb +1867 -1184
  25. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  26. data/lib/net/imap/sasl/authentication_exchange.rb +139 -0
  27. data/lib/net/imap/sasl/authenticators.rb +122 -0
  28. data/lib/net/imap/sasl/client_adapter.rb +123 -0
  29. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +24 -14
  30. data/lib/net/imap/sasl/digest_md5_authenticator.rb +342 -0
  31. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  32. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  33. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +28 -18
  34. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  35. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  36. data/lib/net/imap/sasl/protocol_adapters.rb +101 -0
  37. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  38. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  39. data/lib/net/imap/sasl/stringprep.rb +6 -66
  40. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  41. data/lib/net/imap/sasl.rb +148 -44
  42. data/lib/net/imap/sasl_adapter.rb +20 -0
  43. data/lib/net/imap/search_result.rb +146 -0
  44. data/lib/net/imap/sequence_set.rb +1565 -0
  45. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  46. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  47. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  48. data/lib/net/imap/stringprep/tables.rb +146 -0
  49. data/lib/net/imap/stringprep/trace.rb +85 -0
  50. data/lib/net/imap/stringprep.rb +159 -0
  51. data/lib/net/imap/uidplus_data.rb +244 -0
  52. data/lib/net/imap/vanished_data.rb +56 -0
  53. data/lib/net/imap.rb +2090 -823
  54. data/net-imap.gemspec +7 -8
  55. data/rakelib/benchmarks.rake +91 -0
  56. data/rakelib/rfcs.rake +2 -0
  57. data/rakelib/saslprep.rake +4 -4
  58. data/rakelib/string_prep_tables_generator.rb +84 -60
  59. data/sample/net-imap.rb +167 -0
  60. metadata +45 -49
  61. data/.github/dependabot.yml +0 -6
  62. data/.github/workflows/test.yml +0 -31
  63. data/.gitignore +0 -10
  64. data/benchmarks/stringprep.yml +0 -65
  65. data/benchmarks/table-regexps.yml +0 -39
  66. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  67. data/lib/net/imap/authenticators/plain.rb +0 -41
  68. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  69. data/lib/net/imap/sasl/saslprep.rb +0 -55
  70. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  71. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
data/lib/net/imap/sasl.rb CHANGED
@@ -5,13 +5,12 @@ module Net
5
5
 
6
6
  # Pluggable authentication mechanisms for protocols which support SASL
7
7
  # (Simple Authentication and Security Layer), such as IMAP4, SMTP, LDAP, and
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."
8
+ # XMPP. {RFC-4422}[https://www.rfc-editor.org/rfc/rfc4422] specifies the
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,163 @@ 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 the server ended
118
+ # 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
+ # Creates a new SASL authenticator, using SASL::Authenticators#new.
163
+ #
164
+ # +registry+ defaults to SASL.authenticators. All other arguments are
165
+ # forwarded to to <tt>registry.new</tt>.
166
+ def self.authenticator(*args, registry: authenticators, **kwargs, &block)
167
+ registry.new(*args, **kwargs, &block)
60
168
  end
61
169
 
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
170
+ # Delegates to ::authenticators. See Authenticators#add_authenticator.
171
+ def self.add_authenticator(...) authenticators.add_authenticator(...) end
172
+
173
+ module_function
67
174
 
68
- # See SASLprep#saslprep.
175
+ # See Net::IMAP::StringPrep::SASLprep#saslprep.
69
176
  def saslprep(string, **opts)
70
- SASLprep.saslprep(string, **opts)
177
+ Net::IMAP::StringPrep::SASLprep.saslprep(string, **opts)
71
178
  end
72
179
 
73
180
  end
74
181
  end
75
-
76
182
  end
77
-
78
- Net::IMAP.extend Net::IMAP::SASL
@@ -0,0 +1,20 @@
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 drop_connection; client.logout! end
16
+ def drop_connection!; client.disconnect end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,146 @@
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 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 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
+ end
33
+
34
+ # Returns whether +other+ is a SearchResult with the same values and the
35
+ # same #modseq. The order of numbers is irrelevant.
36
+ #
37
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
38
+ # Net::IMAP::SearchResult[123, 456, modseq: 789]
39
+ # # => true
40
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
41
+ # Net::IMAP::SearchResult[456, 123, modseq: 789]
42
+ # # => true
43
+ #
44
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
45
+ # Net::IMAP::SearchResult[987, 654, modseq: 789]
46
+ # # => false
47
+ # Net::IMAP::SearchResult[123, 456, modseq: 789] ==
48
+ # Net::IMAP::SearchResult[1, 2, 3, modseq: 9999]
49
+ # # => false
50
+ #
51
+ # SearchResult can be compared directly with Array, if #modseq is nil and
52
+ # the array is sorted.
53
+ #
54
+ # Net::IMAP::SearchResult[9, 8, 6, 4, 1] == [1, 4, 6, 8, 9] # => true
55
+ # Net::IMAP::SearchResult[3, 5, 7, modseq: 99] == [3, 5, 7] # => false
56
+ #
57
+ # Note that Array#== does require matching order and ignores #modseq.
58
+ #
59
+ # [9, 8, 6, 4, 1] == Net::IMAP::SearchResult[1, 4, 6, 8, 9] # => false
60
+ # [3, 5, 7] == Net::IMAP::SearchResult[3, 5, 7, modseq: 99] # => true
61
+ #
62
+ def ==(other)
63
+ (modseq ?
64
+ other.is_a?(self.class) && modseq == other.modseq :
65
+ other.is_a?(Array)) &&
66
+ size == other.size &&
67
+ sort == other.sort
68
+ end
69
+
70
+ # Hash equality. Unlike #==, order will be taken into account.
71
+ def hash
72
+ return super if modseq.nil?
73
+ [super, self.class, modseq].hash
74
+ end
75
+
76
+ # Hash equality. Unlike #==, order will be taken into account.
77
+ def eql?(other)
78
+ return super if modseq.nil?
79
+ self.class == other.class && hash == other.hash
80
+ end
81
+
82
+ # Returns a string that represents the SearchResult.
83
+ #
84
+ # Net::IMAP::SearchResult[123, 456, 789].inspect
85
+ # # => "[123, 456, 789]"
86
+ #
87
+ # Net::IMAP::SearchResult[543, 210, 678, modseq: 2048].inspect
88
+ # # => "Net::IMAP::SearchResult[543, 210, 678, modseq: 2048]"
89
+ #
90
+ def inspect
91
+ return super if modseq.nil?
92
+ "%s[%s, modseq: %p]" % [self.class, join(", "), modseq]
93
+ end
94
+
95
+ # Returns a string that follows the formal \IMAP syntax.
96
+ #
97
+ # data = Net::IMAP::SearchResult[2, 8, 32, 128, 256, 512]
98
+ # data.to_s # => "* SEARCH 2 8 32 128 256 512"
99
+ # data.to_s("SEARCH") # => "* SEARCH 2 8 32 128 256 512"
100
+ # data.to_s("SORT") # => "* SORT 2 8 32 128 256 512"
101
+ # data.to_s(nil) # => "2 8 32 128 256 512"
102
+ #
103
+ # data = Net::IMAP::SearchResult[1, 3, 16, 1024, modseq: 2048]
104
+ # data.to_s # => "* SEARCH 1 3 16 1024 (MODSEQ 2048)"
105
+ # data.to_s("SORT") # => "* SORT 1 3 16 1024 (MODSEQ 2048)"
106
+ # data.to_s(nil) # => "1 3 16 1024 (MODSEQ 2048)"
107
+ #
108
+ def to_s(type = "SEARCH")
109
+ str = +""
110
+ str << "* %s " % [type.to_str] unless type.nil?
111
+ str << join(" ")
112
+ str << " (MODSEQ %d)" % [modseq] if modseq
113
+ -str
114
+ end
115
+
116
+ # Converts the SearchResult into a SequenceSet.
117
+ #
118
+ # Net::IMAP::SearchResult[9, 1, 2, 4, 10, 12, 3, modseq: 123_456]
119
+ # .to_sequence_set
120
+ # # => Net::IMAP::SequenceSet["1:4,9:10,12"]
121
+ def to_sequence_set; SequenceSet[*self] end
122
+
123
+ def pretty_print(pp)
124
+ return super if modseq.nil?
125
+ pp.text self.class.name + "["
126
+ pp.group_sub do
127
+ pp.nest(2) do
128
+ pp.breakable ""
129
+ each do |num|
130
+ pp.pp num
131
+ pp.text ","
132
+ pp.fill_breakable
133
+ end
134
+ pp.breakable ""
135
+ pp.text "modseq: "
136
+ pp.pp modseq
137
+ end
138
+ pp.breakable ""
139
+ pp.text "]"
140
+ end
141
+ end
142
+
143
+ end
144
+
145
+ end
146
+ end