email_address 0.1.2 → 0.2.0
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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +18 -0
- data/Gemfile +1 -1
- data/README.md +255 -123
- data/Rakefile +2 -3
- data/email_address.gemspec +27 -22
- data/lib/email_address/active_record_validator.rb +10 -11
- data/lib/email_address/address.rb +126 -80
- data/lib/email_address/canonical_email_address_type.rb +16 -12
- data/lib/email_address/config.rb +102 -51
- data/lib/email_address/email_address_type.rb +17 -13
- data/lib/email_address/exchanger.rb +44 -33
- data/lib/email_address/host.rb +217 -105
- data/lib/email_address/local.rb +127 -87
- data/lib/email_address/messages.yaml +21 -0
- data/lib/email_address/rewriter.rb +144 -0
- data/lib/email_address/version.rb +1 -1
- data/lib/email_address.rb +48 -53
- data/test/activerecord/test_ar.rb +17 -13
- data/test/activerecord/user.rb +31 -30
- data/test/email_address/test_address.rb +84 -21
- data/test/email_address/test_config.rb +10 -10
- data/test/email_address/test_exchanger.rb +6 -7
- data/test/email_address/test_host.rb +59 -21
- data/test/email_address/test_local.rb +49 -36
- data/test/email_address/test_rewriter.rb +11 -0
- data/test/test_aliasing.rb +53 -0
- data/test/test_email_address.rb +15 -19
- data/test/test_helper.rb +9 -8
- metadata +43 -21
- data/.travis.yml +0 -10
data/lib/email_address/host.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require "simpleidn"
|
2
|
+
require "resolv"
|
3
|
+
require "net/smtp"
|
4
4
|
|
5
5
|
module EmailAddress
|
6
6
|
##############################################################################
|
@@ -32,16 +32,16 @@ module EmailAddress
|
|
32
32
|
class Host
|
33
33
|
attr_reader :host_name
|
34
34
|
attr_accessor :dns_name, :domain_name, :registration_name,
|
35
|
-
|
36
|
-
|
35
|
+
:tld, :tld2, :subdomains, :ip_address, :config, :provider,
|
36
|
+
:comment, :error_message, :reason, :locale
|
37
37
|
MAX_HOST_LENGTH = 255
|
38
38
|
|
39
39
|
# Sometimes, you just need a Regexp...
|
40
|
-
DNS_HOST_REGEX
|
40
|
+
DNS_HOST_REGEX = / [\p{L}\p{N}]+ (?: (?: -{1,2} | \.) [\p{L}\p{N}]+ )*/x
|
41
41
|
|
42
42
|
# The IPv4 and IPv6 were lifted from Resolv::IPv?::Regex and tweaked to not
|
43
43
|
# \A...\z anchor at the edges.
|
44
|
-
|
44
|
+
IPV6_HOST_REGEX = /\[IPv6:
|
45
45
|
(?: (?:(?x-mi:
|
46
46
|
(?:[0-9A-Fa-f]{1,4}:){7}
|
47
47
|
[0-9A-Fa-f]{1,4}
|
@@ -60,7 +60,7 @@ module EmailAddress
|
|
60
60
|
(?: \d+)\.(?: \d+)\.(?: \d+)\.(?: \d+)
|
61
61
|
)))\]/ix
|
62
62
|
|
63
|
-
|
63
|
+
IPV4_HOST_REGEX = /\[((?x-mi:0
|
64
64
|
|1(?:[0-9][0-9]?)?
|
65
65
|
|2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
|
66
66
|
|[3-9][0-9]?))\.((?x-mi:0
|
@@ -79,49 +79,50 @@ module EmailAddress
|
|
79
79
|
|
80
80
|
# Matches Host forms: DNS name, IPv4, or IPv6 formats
|
81
81
|
STANDARD_HOST_REGEX = /\A (?: #{DNS_HOST_REGEX}
|
82
|
-
| #{
|
82
|
+
| #{IPV4_HOST_REGEX} | #{IPV6_HOST_REGEX}) \z/ix
|
83
83
|
|
84
84
|
# host name -
|
85
85
|
# * host type - :email for an email host, :mx for exchanger host
|
86
|
-
def initialize(host_name, config={})
|
87
|
-
@original
|
86
|
+
def initialize(host_name, config = {}, locale = "en")
|
87
|
+
@original = host_name ||= ""
|
88
|
+
@locale = locale
|
88
89
|
config[:host_type] ||= :email
|
89
|
-
@config
|
90
|
+
@config = config.is_a?(Hash) ? Config.new(config) : config
|
91
|
+
@error = @error_message = nil
|
90
92
|
parse(host_name)
|
91
93
|
end
|
92
94
|
|
93
95
|
# Returns the String representation of the host name (or IP)
|
94
96
|
def name
|
95
|
-
if
|
96
|
-
"[#{
|
97
|
-
elsif
|
98
|
-
"[IPv6:#{
|
97
|
+
if ipv4?
|
98
|
+
"[#{ip_address}]"
|
99
|
+
elsif ipv6?
|
100
|
+
"[IPv6:#{ip_address}]"
|
99
101
|
elsif @config[:host_encoding] && @config[:host_encoding] == :unicode
|
100
|
-
::SimpleIDN.to_unicode(
|
102
|
+
::SimpleIDN.to_unicode(host_name)
|
101
103
|
else
|
102
|
-
|
104
|
+
dns_name
|
103
105
|
end
|
104
106
|
end
|
105
|
-
|
107
|
+
alias_method :to_s, :name
|
106
108
|
|
107
109
|
# The canonical host name is the simplified, DNS host name
|
108
110
|
def canonical
|
109
|
-
|
111
|
+
dns_name
|
110
112
|
end
|
111
113
|
|
112
114
|
# Returns the munged version of the name, replacing everything after the
|
113
115
|
# initial two characters with "*****" or the configured "munge_string".
|
114
116
|
def munge
|
115
|
-
|
117
|
+
host_name.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
|
116
118
|
end
|
117
119
|
|
118
120
|
############################################################################
|
119
121
|
# Parsing
|
120
122
|
############################################################################
|
121
123
|
|
122
|
-
|
123
124
|
def parse(host) # :nodoc:
|
124
|
-
host =
|
125
|
+
host = parse_comment(host)
|
125
126
|
|
126
127
|
if host =~ /\A\[IPv6:(.+)\]/i
|
127
128
|
self.ip_address = $1
|
@@ -143,62 +144,96 @@ module EmailAddress
|
|
143
144
|
end
|
144
145
|
|
145
146
|
def host_name=(name)
|
146
|
-
|
147
|
-
@
|
147
|
+
name = fully_qualified_domain_name(name.downcase)
|
148
|
+
@host_name = name
|
149
|
+
if @config[:host_remove_spaces]
|
150
|
+
@host_name = @host_name.delete(" ")
|
151
|
+
end
|
152
|
+
@dns_name = if /[^[:ascii:]]/.match?(host_name)
|
153
|
+
::SimpleIDN.to_ascii(host_name)
|
154
|
+
else
|
155
|
+
host_name
|
156
|
+
end
|
148
157
|
|
149
158
|
# Subdomain only (root@localhost)
|
150
|
-
if name.index(
|
159
|
+
if name.index(".").nil?
|
151
160
|
self.subdomains = name
|
152
161
|
|
153
162
|
# Split sub.domain from .tld: *.com, *.xx.cc, *.cc
|
154
163
|
elsif name =~ /\A(.+)\.(\w{3,10})\z/ ||
|
155
|
-
|
156
|
-
|
164
|
+
name =~ /\A(.+)\.(\w{1,3}\.\w\w)\z/ ||
|
165
|
+
name =~ /\A(.+)\.(\w\w)\z/
|
157
166
|
|
158
167
|
sub_and_domain, self.tld2 = [$1, $2] # sub+domain, com || co.uk
|
159
|
-
self.tld =
|
168
|
+
self.tld = tld2.sub(/\A.+\./, "") # co.uk => uk
|
160
169
|
if sub_and_domain =~ /\A(.+)\.(.+)\z/ # is subdomain? sub.example [.tld2]
|
161
|
-
self.subdomains
|
170
|
+
self.subdomains = $1
|
162
171
|
self.registration_name = $2
|
163
172
|
else
|
164
173
|
self.registration_name = sub_and_domain
|
165
|
-
#self.domain_name = sub_and_domain + '.' + self.tld2
|
174
|
+
# self.domain_name = sub_and_domain + '.' + self.tld2
|
166
175
|
end
|
167
|
-
self.domain_name =
|
168
|
-
|
176
|
+
self.domain_name = registration_name + "." + tld2
|
177
|
+
find_provider
|
178
|
+
else # Bad format
|
179
|
+
self.subdomains = self.tld = self.tld2 = ""
|
180
|
+
self.domain_name = self.registration_name = name
|
169
181
|
end
|
170
182
|
end
|
171
183
|
|
172
|
-
def
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
184
|
+
def fully_qualified_domain_name(host_part)
|
185
|
+
dn = @config[:address_fqdn_domain]
|
186
|
+
if !dn
|
187
|
+
if (host_part.nil? || host_part <= " ") && @config[:host_local]
|
188
|
+
"localhost"
|
189
|
+
else
|
190
|
+
host_part
|
178
191
|
end
|
192
|
+
elsif host_part.nil? || host_part <= " "
|
193
|
+
dn
|
194
|
+
elsif !host_part.include?(".")
|
195
|
+
host_part + "." + dn
|
196
|
+
else
|
197
|
+
host_part
|
179
198
|
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# True if host is hosted at the provider, not a public provider host name
|
202
|
+
def hosted_service?
|
203
|
+
return false unless registration_name
|
204
|
+
find_provider
|
205
|
+
return false unless config[:host_match]
|
206
|
+
!matches?(config[:host_match])
|
207
|
+
end
|
180
208
|
|
181
|
-
|
209
|
+
def find_provider # :nodoc:
|
210
|
+
return provider if provider
|
182
211
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
212
|
+
Config.providers.each do |provider, config|
|
213
|
+
if config[:host_match] && matches?(config[:host_match])
|
214
|
+
return set_provider(provider, config)
|
215
|
+
end
|
187
216
|
end
|
188
217
|
|
189
|
-
|
218
|
+
return set_provider(:default) unless dns_enabled?
|
219
|
+
|
220
|
+
self.provider ||= set_provider(:default)
|
190
221
|
end
|
191
222
|
|
192
|
-
def set_provider(name, provider_config={}) # :nodoc:
|
193
|
-
|
194
|
-
|
223
|
+
def set_provider(name, provider_config = {}) # :nodoc:
|
224
|
+
config.configure(provider_config)
|
225
|
+
@provider = name
|
195
226
|
end
|
196
227
|
|
197
228
|
# Returns a hash of the parts of the host name after parsing.
|
198
229
|
def parts
|
199
|
-
{
|
200
|
-
|
201
|
-
|
230
|
+
{host_name: host_name, dns_name: dns_name, subdomain: subdomains,
|
231
|
+
registration_name: registration_name, domain_name: domain_name,
|
232
|
+
tld2: tld2, tld: tld, ip_address: ip_address}
|
233
|
+
end
|
234
|
+
|
235
|
+
def hosted_provider
|
236
|
+
Exchanger.cached(dns_name).provider
|
202
237
|
end
|
203
238
|
|
204
239
|
############################################################################
|
@@ -207,19 +242,19 @@ module EmailAddress
|
|
207
242
|
|
208
243
|
# Is this a fully-qualified domain name?
|
209
244
|
def fqdn?
|
210
|
-
|
245
|
+
tld ? true : false
|
211
246
|
end
|
212
247
|
|
213
248
|
def ip?
|
214
|
-
|
249
|
+
!!ip_address
|
215
250
|
end
|
216
251
|
|
217
252
|
def ipv4?
|
218
|
-
|
253
|
+
ip? && ip_address.include?(".")
|
219
254
|
end
|
220
255
|
|
221
256
|
def ipv6?
|
222
|
-
|
257
|
+
ip? && ip_address.include?(":")
|
223
258
|
end
|
224
259
|
|
225
260
|
############################################################################
|
@@ -237,25 +272,25 @@ module EmailAddress
|
|
237
272
|
rules = Array(rules)
|
238
273
|
return false if rules.empty?
|
239
274
|
rules.each do |rule|
|
240
|
-
return rule if rule ==
|
275
|
+
return rule if rule == domain_name || rule == dns_name
|
241
276
|
return rule if registration_name_matches?(rule)
|
242
277
|
return rule if tld_matches?(rule)
|
243
278
|
return rule if domain_matches?(rule)
|
244
279
|
return rule if self.provider && provider_matches?(rule)
|
245
|
-
return rule if
|
280
|
+
return rule if ip_matches?(rule)
|
246
281
|
end
|
247
282
|
false
|
248
283
|
end
|
249
284
|
|
250
285
|
# Does "example." match any tld?
|
251
286
|
def registration_name_matches?(rule)
|
252
|
-
|
287
|
+
rule == "#{registration_name}."
|
253
288
|
end
|
254
289
|
|
255
290
|
# Does "sub.example.com" match ".com" and ".example.com" top level names?
|
256
291
|
# Matches TLD (uk) or TLD2 (co.uk)
|
257
292
|
def tld_matches?(rule)
|
258
|
-
rule.match(/\A\.(.+)\z/) && ($1 ==
|
293
|
+
rule.match(/\A\.(.+)\z/) && ($1 == tld || $1 == tld2) # ? true : false
|
259
294
|
end
|
260
295
|
|
261
296
|
def provider_matches?(rule)
|
@@ -266,24 +301,17 @@ module EmailAddress
|
|
266
301
|
# Requires optionally starts with a "@".
|
267
302
|
def domain_matches?(rule)
|
268
303
|
rule = $1 if rule =~ /\A@(.+)/
|
269
|
-
return rule if File.fnmatch?(rule,
|
270
|
-
return rule if File.fnmatch?(rule,
|
304
|
+
return rule if domain_name && File.fnmatch?(rule, domain_name)
|
305
|
+
return rule if dns_name && File.fnmatch?(rule, dns_name)
|
271
306
|
false
|
272
307
|
end
|
273
308
|
|
274
309
|
# True if the host is an IP Address form, and that address matches
|
275
310
|
# the passed CIDR string ("10.9.8.0/24" or "2001:..../64")
|
276
311
|
def ip_matches?(cidr)
|
277
|
-
return false unless
|
278
|
-
|
279
|
-
|
280
|
-
c = NetAddr::CIDR.create(cidr)
|
281
|
-
if cidr.include?(":") && self.ip_address.include?(":")
|
282
|
-
return cidr if c.matches?(self.ip_address)
|
283
|
-
elsif cidr.include?(".") && self.ip_address.include?(".")
|
284
|
-
return cidr if c.matches?(self.ip_address)
|
285
|
-
end
|
286
|
-
false
|
312
|
+
return false unless ip_address
|
313
|
+
net = IPAddr.new(cidr)
|
314
|
+
net.include?(IPAddr.new(ip_address))
|
287
315
|
end
|
288
316
|
|
289
317
|
############################################################################
|
@@ -292,45 +320,50 @@ module EmailAddress
|
|
292
320
|
|
293
321
|
# True if the :dns_lookup setting is enabled
|
294
322
|
def dns_enabled?
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
# True if the host name has a DNS A Record
|
299
|
-
def has_dns_a_record?
|
300
|
-
dns_a_record.size > 0 ? true : false
|
323
|
+
return false if @config[:dns_lookup] == :off
|
324
|
+
return false if @config[:host_validation] == :syntax
|
325
|
+
true
|
301
326
|
end
|
302
327
|
|
303
328
|
# Returns: [official_hostname, alias_hostnames, address_family, *address_list]
|
304
329
|
def dns_a_record
|
305
|
-
@_dns_a_record
|
330
|
+
@_dns_a_record = "0.0.0.0" if @config[:dns_lookup] == :off
|
331
|
+
@_dns_a_record ||= Addrinfo.getaddrinfo(dns_name, 80) # Port 80 for A rec, 25 for MX
|
306
332
|
rescue SocketError # not found, but could also mean network not work
|
307
333
|
@_dns_a_record ||= []
|
308
334
|
end
|
309
335
|
|
310
|
-
# Returns an array of
|
336
|
+
# Returns an array of Exchanger hosts configured in DNS.
|
311
337
|
# The array will be empty if none are configured.
|
312
338
|
def exchangers
|
313
|
-
return nil if @config[:host_type] != :email || !self.dns_enabled?
|
314
|
-
@_exchangers ||=
|
339
|
+
# return nil if @config[:host_type] != :email || !self.dns_enabled?
|
340
|
+
@_exchangers ||= Exchanger.cached(dns_name, @config)
|
315
341
|
end
|
316
342
|
|
317
343
|
# Returns a DNS TXT Record
|
318
|
-
def txt(alternate_host=nil)
|
344
|
+
def txt(alternate_host = nil)
|
345
|
+
return nil unless dns_enabled?
|
319
346
|
Resolv::DNS.open do |dns|
|
320
|
-
|
321
|
-
|
347
|
+
dns.timeouts = @config[:dns_timeout] if @config[:dns_timeout]
|
348
|
+
records = begin
|
349
|
+
dns.getresources(alternate_host || dns_name,
|
350
|
+
Resolv::DNS::Resource::IN::TXT)
|
351
|
+
rescue Resolv::ResolvTimeout
|
352
|
+
[]
|
353
|
+
end
|
354
|
+
|
322
355
|
records.empty? ? nil : records.map(&:data).join(" ")
|
323
356
|
end
|
324
357
|
end
|
325
358
|
|
326
359
|
# Parses TXT record pairs into a hash
|
327
|
-
def txt_hash(alternate_host=nil)
|
360
|
+
def txt_hash(alternate_host = nil)
|
328
361
|
fields = {}
|
329
|
-
record =
|
362
|
+
record = txt(alternate_host)
|
330
363
|
return fields unless record
|
331
364
|
|
332
365
|
record.split(/\s*;\s*/).each do |pair|
|
333
|
-
(n,v) = pair.split(/\s*=\s*/)
|
366
|
+
(n, v) = pair.split(/\s*=\s*/)
|
334
367
|
fields[n.to_sym] = v
|
335
368
|
end
|
336
369
|
fields
|
@@ -339,7 +372,7 @@ module EmailAddress
|
|
339
372
|
# Returns a hash of the domain's DMARC (https://en.wikipedia.org/wiki/DMARC)
|
340
373
|
# settings.
|
341
374
|
def dmarc
|
342
|
-
|
375
|
+
dns_name ? txt_hash("_dmarc." + dns_name) : {}
|
343
376
|
end
|
344
377
|
|
345
378
|
############################################################################
|
@@ -347,34 +380,113 @@ module EmailAddress
|
|
347
380
|
############################################################################
|
348
381
|
|
349
382
|
# Returns true if the host name is valid according to the current configuration
|
350
|
-
def valid?(
|
351
|
-
|
383
|
+
def valid?(rules = {})
|
384
|
+
host_validation = rules[:host_validation] || @config[:host_validation] || :mx
|
385
|
+
dns_lookup = rules[:dns_lookup] || host_validation
|
386
|
+
self.error_message = nil
|
387
|
+
if ip_address
|
388
|
+
valid_ip?
|
389
|
+
elsif !valid_format?
|
390
|
+
false
|
391
|
+
elsif dns_lookup == :connect
|
392
|
+
valid_mx? && connect
|
393
|
+
elsif dns_lookup == :mx
|
394
|
+
valid_mx?
|
395
|
+
elsif dns_lookup == :a
|
396
|
+
valid_dns?
|
397
|
+
else
|
352
398
|
true
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# True if the host name has a DNS A Record
|
403
|
+
def valid_dns?
|
404
|
+
return true unless dns_enabled?
|
405
|
+
bool = dns_a_record.size > 0 || set_error(:domain_unknown)
|
406
|
+
if localhost? && !@config[:host_local]
|
407
|
+
bool = set_error(:domain_no_localhost)
|
408
|
+
end
|
409
|
+
bool
|
410
|
+
end
|
411
|
+
|
412
|
+
# True if the host name has valid MX servers configured in DNS
|
413
|
+
def valid_mx?
|
414
|
+
return true unless dns_enabled?
|
415
|
+
if exchangers.nil?
|
416
|
+
set_error(:domain_unknown)
|
417
|
+
elsif exchangers.mx_ips.size > 0
|
418
|
+
if localhost? && !@config[:host_local]
|
419
|
+
set_error(:domain_no_localhost)
|
420
|
+
else
|
421
|
+
true
|
422
|
+
end
|
423
|
+
elsif @config[:dns_timeout].nil? && valid_dns?
|
424
|
+
set_error(:domain_does_not_accept_email)
|
361
425
|
else
|
362
|
-
|
426
|
+
set_error(:domain_unknown)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# True if the host_name passes Regular Expression match and size limits.
|
431
|
+
def valid_format?
|
432
|
+
if host_name =~ CANONICAL_HOST_REGEX && to_s.size <= MAX_HOST_LENGTH
|
433
|
+
return true if localhost?
|
434
|
+
return true if host_name.include?(".") # require FQDN
|
363
435
|
end
|
436
|
+
set_error(:domain_invalid)
|
364
437
|
end
|
365
438
|
|
366
439
|
# Returns true if the IP address given in that form of the host name
|
367
440
|
# is a potentially valid IP address. It does not check if the address
|
368
441
|
# is reachable.
|
369
442
|
def valid_ip?
|
370
|
-
if
|
371
|
-
|
372
|
-
elsif
|
373
|
-
|
374
|
-
elsif
|
375
|
-
|
443
|
+
if !@config[:host_allow_ip]
|
444
|
+
bool = set_error(:ip_address_forbidden)
|
445
|
+
elsif ip_address.include?(":")
|
446
|
+
bool = ip_address.match(Resolv::IPv6::Regex) ? true : set_error(:ipv6_address_invalid)
|
447
|
+
elsif ip_address.include?(".")
|
448
|
+
bool = ip_address.match(Resolv::IPv4::Regex) ? true : set_error(:ipv4_address_invalid)
|
449
|
+
end
|
450
|
+
if bool && (localhost? && !@config[:host_local])
|
451
|
+
bool = set_error(:ip_address_no_localhost)
|
452
|
+
end
|
453
|
+
bool
|
454
|
+
end
|
455
|
+
|
456
|
+
def localhost?
|
457
|
+
return true if host_name == "localhost"
|
458
|
+
return false unless ip_address
|
459
|
+
IPAddr.new(ip_address).loopback?
|
460
|
+
end
|
461
|
+
|
462
|
+
# Connects to host to test it can receive email. This should NOT be performed
|
463
|
+
# as an email address check, but is provided to assist in problem resolution.
|
464
|
+
# If you abuse this, you *could* be blocked by the ESP.
|
465
|
+
def connect
|
466
|
+
smtp = Net::SMTP.new(host_name || ip_address)
|
467
|
+
smtp.start(@config[:helo_name] || "localhost")
|
468
|
+
smtp.finish
|
469
|
+
true
|
470
|
+
rescue Net::SMTPFatalError => e
|
471
|
+
set_error(:server_not_available, e.to_s)
|
472
|
+
rescue SocketError => e
|
473
|
+
set_error(:server_not_available, e.to_s)
|
474
|
+
ensure
|
475
|
+
if smtp&.started?
|
476
|
+
smtp.finish
|
376
477
|
end
|
377
478
|
end
|
378
479
|
|
480
|
+
def set_error(err, reason = nil)
|
481
|
+
@error = err
|
482
|
+
@reason = reason
|
483
|
+
@error_message = Config.error_message(err, locale)
|
484
|
+
false
|
485
|
+
end
|
486
|
+
|
487
|
+
# The inverse of valid? -- Returns nil (falsey) if valid, otherwise error message
|
488
|
+
def error
|
489
|
+
valid? ? nil : @error_message
|
490
|
+
end
|
379
491
|
end
|
380
492
|
end
|