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.
- checksums.yaml +4 -4
- data/BSDL +22 -0
- data/COPYING +56 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +3 -22
- data/README.md +25 -8
- data/Rakefile +0 -7
- data/docs/styles.css +72 -23
- data/lib/net/imap/authenticators.rb +26 -57
- data/lib/net/imap/command_data.rb +74 -54
- data/lib/net/imap/config/attr_accessors.rb +75 -0
- data/lib/net/imap/config/attr_inheritance.rb +90 -0
- data/lib/net/imap/config/attr_type_coercion.rb +61 -0
- data/lib/net/imap/config.rb +470 -0
- data/lib/net/imap/data_encoding.rb +21 -9
- data/lib/net/imap/data_lite.rb +226 -0
- data/lib/net/imap/deprecated_client_options.rb +142 -0
- data/lib/net/imap/errors.rb +27 -1
- data/lib/net/imap/esearch_result.rb +180 -0
- data/lib/net/imap/fetch_data.rb +597 -0
- data/lib/net/imap/flags.rb +1 -1
- data/lib/net/imap/response_data.rb +250 -440
- data/lib/net/imap/response_parser/parser_utils.rb +245 -0
- data/lib/net/imap/response_parser.rb +1867 -1184
- data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
- data/lib/net/imap/sasl/authentication_exchange.rb +139 -0
- data/lib/net/imap/sasl/authenticators.rb +122 -0
- data/lib/net/imap/sasl/client_adapter.rb +123 -0
- data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +24 -14
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +342 -0
- data/lib/net/imap/sasl/external_authenticator.rb +83 -0
- data/lib/net/imap/sasl/gs2_header.rb +80 -0
- data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +28 -18
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
- data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
- data/lib/net/imap/sasl/protocol_adapters.rb +101 -0
- data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
- data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
- data/lib/net/imap/sasl/stringprep.rb +6 -66
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
- data/lib/net/imap/sasl.rb +148 -44
- data/lib/net/imap/sasl_adapter.rb +20 -0
- data/lib/net/imap/search_result.rb +146 -0
- data/lib/net/imap/sequence_set.rb +1565 -0
- data/lib/net/imap/stringprep/nameprep.rb +70 -0
- data/lib/net/imap/stringprep/saslprep.rb +69 -0
- data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
- data/lib/net/imap/stringprep/tables.rb +146 -0
- data/lib/net/imap/stringprep/trace.rb +85 -0
- data/lib/net/imap/stringprep.rb +159 -0
- data/lib/net/imap/uidplus_data.rb +244 -0
- data/lib/net/imap/vanished_data.rb +56 -0
- data/lib/net/imap.rb +2090 -823
- data/net-imap.gemspec +7 -8
- data/rakelib/benchmarks.rake +91 -0
- data/rakelib/rfcs.rake +2 -0
- data/rakelib/saslprep.rake +4 -4
- data/rakelib/string_prep_tables_generator.rb +84 -60
- data/sample/net-imap.rb +167 -0
- metadata +45 -49
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/test.yml +0 -31
- data/.gitignore +0 -10
- data/benchmarks/stringprep.yml +0 -65
- data/benchmarks/table-regexps.yml +0 -39
- data/lib/net/imap/authenticators/digest_md5.rb +0 -115
- data/lib/net/imap/authenticators/plain.rb +0 -41
- data/lib/net/imap/authenticators/xoauth2.rb +0 -20
- data/lib/net/imap/sasl/saslprep.rb +0 -55
- data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
- 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://
|
9
|
-
# common SASL framework
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
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
|
-
#
|
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
|
-
|
29
|
-
|
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
|
-
#
|
32
|
-
#
|
33
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
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
|
-
#
|
58
|
-
|
59
|
-
|
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
|
-
|
64
|
-
|
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
|