net-imap 0.2.1 → 0.2.2
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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +3 -5
- data/.gitignore +1 -0
- data/README.md +1 -1
- data/lib/net/imap.rb +149 -2702
- data/lib/net/imap/authenticators.rb +44 -0
- data/lib/net/imap/authenticators/cram_md5.rb +49 -0
- data/lib/net/imap/authenticators/digest_md5.rb +111 -0
- data/lib/net/imap/authenticators/login.rb +43 -0
- data/lib/net/imap/authenticators/plain.rb +41 -0
- data/lib/net/imap/command_data.rb +301 -0
- data/lib/net/imap/data_encoding.rb +47 -0
- data/lib/net/imap/flags.rb +76 -0
- data/lib/net/imap/response_data.rb +527 -0
- data/lib/net/imap/response_parser.rb +1530 -0
- metadata +12 -3
- data/Gemfile.lock +0 -23
@@ -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,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 "PLAIN", 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,301 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP < Protocol
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def validate_data(data)
|
9
|
+
case data
|
10
|
+
when nil
|
11
|
+
when String
|
12
|
+
when Integer
|
13
|
+
NumValidator.ensure_number(data)
|
14
|
+
when Array
|
15
|
+
if data[0] == 'CHANGEDSINCE'
|
16
|
+
NumValidator.ensure_mod_sequence_value(data[1])
|
17
|
+
else
|
18
|
+
data.each do |i|
|
19
|
+
validate_data(i)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
when Time
|
23
|
+
when Symbol
|
24
|
+
else
|
25
|
+
data.validate
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def send_data(data, tag = nil)
|
30
|
+
case data
|
31
|
+
when nil
|
32
|
+
put_string("NIL")
|
33
|
+
when String
|
34
|
+
send_string_data(data, tag)
|
35
|
+
when Integer
|
36
|
+
send_number_data(data)
|
37
|
+
when Array
|
38
|
+
send_list_data(data, tag)
|
39
|
+
when Time
|
40
|
+
send_time_data(data)
|
41
|
+
when Symbol
|
42
|
+
send_symbol_data(data)
|
43
|
+
else
|
44
|
+
data.send_data(self, tag)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_string_data(str, tag = nil)
|
49
|
+
case str
|
50
|
+
when ""
|
51
|
+
put_string('""')
|
52
|
+
when /[\x80-\xff\r\n]/n
|
53
|
+
# literal
|
54
|
+
send_literal(str, tag)
|
55
|
+
when /[(){ \x00-\x1f\x7f%*"\\]/n
|
56
|
+
# quoted string
|
57
|
+
send_quoted_string(str)
|
58
|
+
else
|
59
|
+
put_string(str)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def send_quoted_string(str)
|
64
|
+
put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_literal(str, tag = nil)
|
68
|
+
synchronize do
|
69
|
+
put_string("{" + str.bytesize.to_s + "}" + CRLF)
|
70
|
+
@continued_command_tag = tag
|
71
|
+
@continuation_request_exception = nil
|
72
|
+
begin
|
73
|
+
@continuation_request_arrival.wait
|
74
|
+
e = @continuation_request_exception || @exception
|
75
|
+
raise e if e
|
76
|
+
put_string(str)
|
77
|
+
ensure
|
78
|
+
@continued_command_tag = nil
|
79
|
+
@continuation_request_exception = nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def send_number_data(num)
|
85
|
+
put_string(num.to_s)
|
86
|
+
end
|
87
|
+
|
88
|
+
def send_list_data(list, tag = nil)
|
89
|
+
put_string("(")
|
90
|
+
first = true
|
91
|
+
list.each do |i|
|
92
|
+
if first
|
93
|
+
first = false
|
94
|
+
else
|
95
|
+
put_string(" ")
|
96
|
+
end
|
97
|
+
send_data(i, tag)
|
98
|
+
end
|
99
|
+
put_string(")")
|
100
|
+
end
|
101
|
+
|
102
|
+
DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
|
103
|
+
|
104
|
+
def send_time_data(time)
|
105
|
+
t = time.dup.gmtime
|
106
|
+
s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
|
107
|
+
t.day, DATE_MONTH[t.month - 1], t.year,
|
108
|
+
t.hour, t.min, t.sec)
|
109
|
+
put_string(s)
|
110
|
+
end
|
111
|
+
|
112
|
+
def send_symbol_data(symbol)
|
113
|
+
put_string("\\" + symbol.to_s)
|
114
|
+
end
|
115
|
+
|
116
|
+
class RawData # :nodoc:
|
117
|
+
def send_data(imap, tag)
|
118
|
+
imap.__send__(:put_string, @data)
|
119
|
+
end
|
120
|
+
|
121
|
+
def validate
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def initialize(data)
|
127
|
+
@data = data
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class Atom # :nodoc:
|
132
|
+
def send_data(imap, tag)
|
133
|
+
imap.__send__(:put_string, @data)
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def initialize(data)
|
142
|
+
@data = data
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class QuotedString # :nodoc:
|
147
|
+
def send_data(imap, tag)
|
148
|
+
imap.__send__(:send_quoted_string, @data)
|
149
|
+
end
|
150
|
+
|
151
|
+
def validate
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def initialize(data)
|
157
|
+
@data = data
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class Literal # :nodoc:
|
162
|
+
def send_data(imap, tag)
|
163
|
+
imap.__send__(:send_literal, @data, tag)
|
164
|
+
end
|
165
|
+
|
166
|
+
def validate
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def initialize(data)
|
172
|
+
@data = data
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class MessageSet # :nodoc:
|
177
|
+
def send_data(imap, tag)
|
178
|
+
imap.__send__(:put_string, format_internal(@data))
|
179
|
+
end
|
180
|
+
|
181
|
+
def validate
|
182
|
+
validate_internal(@data)
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def initialize(data)
|
188
|
+
@data = data
|
189
|
+
end
|
190
|
+
|
191
|
+
def format_internal(data)
|
192
|
+
case data
|
193
|
+
when "*"
|
194
|
+
return data
|
195
|
+
when Integer
|
196
|
+
if data == -1
|
197
|
+
return "*"
|
198
|
+
else
|
199
|
+
return data.to_s
|
200
|
+
end
|
201
|
+
when Range
|
202
|
+
return format_internal(data.first) +
|
203
|
+
":" + format_internal(data.last)
|
204
|
+
when Array
|
205
|
+
return data.collect {|i| format_internal(i)}.join(",")
|
206
|
+
when ThreadMember
|
207
|
+
return data.seqno.to_s +
|
208
|
+
":" + data.children.collect {|i| format_internal(i).join(",")}
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def validate_internal(data)
|
213
|
+
case data
|
214
|
+
when "*"
|
215
|
+
when Integer
|
216
|
+
NumValidator.ensure_nz_number(data)
|
217
|
+
when Range
|
218
|
+
when Array
|
219
|
+
data.each do |i|
|
220
|
+
validate_internal(i)
|
221
|
+
end
|
222
|
+
when ThreadMember
|
223
|
+
data.children.each do |i|
|
224
|
+
validate_internal(i)
|
225
|
+
end
|
226
|
+
else
|
227
|
+
raise DataFormatError, data.inspect
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class ClientID # :nodoc:
|
233
|
+
|
234
|
+
def send_data(imap, tag)
|
235
|
+
imap.__send__(:send_data, format_internal(@data), tag)
|
236
|
+
end
|
237
|
+
|
238
|
+
def validate
|
239
|
+
validate_internal(@data)
|
240
|
+
end
|
241
|
+
|
242
|
+
private
|
243
|
+
|
244
|
+
def initialize(data)
|
245
|
+
@data = data
|
246
|
+
end
|
247
|
+
|
248
|
+
def validate_internal(client_id)
|
249
|
+
client_id.to_h.each do |k,v|
|
250
|
+
unless StringFormatter.valid_string?(k)
|
251
|
+
raise DataFormatError, client_id.inspect
|
252
|
+
end
|
253
|
+
end
|
254
|
+
rescue NoMethodError, TypeError # to_h failed
|
255
|
+
raise DataFormatError, client_id.inspect
|
256
|
+
end
|
257
|
+
|
258
|
+
def format_internal(client_id)
|
259
|
+
return nil if client_id.nil?
|
260
|
+
client_id.to_h.flat_map {|k,v|
|
261
|
+
[StringFormatter.string(k), StringFormatter.nstring(v)]
|
262
|
+
}
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
266
|
+
|
267
|
+
module StringFormatter
|
268
|
+
|
269
|
+
LITERAL_REGEX = /[\x80-\xff\r\n]/n
|
270
|
+
|
271
|
+
module_function
|
272
|
+
|
273
|
+
# Allows symbols in addition to strings
|
274
|
+
def valid_string?(str)
|
275
|
+
str.is_a?(Symbol) || str.respond_to?(:to_str)
|
276
|
+
end
|
277
|
+
|
278
|
+
# Allows nil, symbols, and strings
|
279
|
+
def valid_nstring?(str)
|
280
|
+
str.nil? || valid_string?(str)
|
281
|
+
end
|
282
|
+
|
283
|
+
# coerces using +to_s+
|
284
|
+
def string(str)
|
285
|
+
str = str.to_s
|
286
|
+
if str =~ LITERAL_REGEX
|
287
|
+
Literal.new(str)
|
288
|
+
else
|
289
|
+
QuotedString.new(str)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# coerces non-nil using +to_s+
|
294
|
+
def nstring(str)
|
295
|
+
str.nil? ? nil : string(str)
|
296
|
+
end
|
297
|
+
|
298
|
+
end
|
299
|
+
|
300
|
+
end
|
301
|
+
end
|