email_address 0.1.16 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +18 -0
- data/Gemfile +1 -1
- data/README.md +65 -6
- data/Rakefile +2 -2
- data/email_address.gemspec +22 -23
- data/lib/email_address.rb +17 -19
- data/lib/email_address/active_record_validator.rb +8 -11
- data/lib/email_address/address.rb +37 -31
- data/lib/email_address/canonical_email_address_type.rb +14 -12
- data/lib/email_address/config.rb +14 -2
- data/lib/email_address/email_address_type.rb +15 -13
- data/lib/email_address/exchanger.rb +8 -22
- data/lib/email_address/host.rb +29 -48
- data/lib/email_address/local.rb +103 -106
- data/lib/email_address/rewriter.rb +28 -31
- data/lib/email_address/version.rb +1 -1
- data/test/activerecord/test_ar.rb +17 -13
- data/test/activerecord/user.rb +31 -30
- data/test/email_address/test_address.rb +49 -25
- data/test/email_address/test_config.rb +8 -8
- data/test/email_address/test_exchanger.rb +6 -7
- data/test/email_address/test_local.rb +35 -35
- data/test/email_address/test_rewriter.rb +2 -5
- data/test/test_aliasing.rb +53 -0
- data/test/test_email_address.rb +14 -18
- data/test/test_helper.rb +9 -8
- metadata +34 -21
- data/.travis.yml +0 -9
@@ -29,20 +29,22 @@
|
|
29
29
|
# user.canonical_email #=> "patsmith@gmail.com"
|
30
30
|
################################################################################
|
31
31
|
|
32
|
-
|
32
|
+
module EmailAddress
|
33
|
+
class CanonicalEmailAddressType < ActiveRecord::Type::Value
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
# From user input, setter
|
36
|
+
def cast(value)
|
37
|
+
super(Address.new(value).canonical)
|
38
|
+
end
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
# From a database value
|
41
|
+
def deserialize(value)
|
42
|
+
value && Address.new(value).normal
|
43
|
+
end
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
45
|
+
# To a database value (string)
|
46
|
+
def serialize(value)
|
47
|
+
value && Address.new(value).normal
|
48
|
+
end
|
47
49
|
end
|
48
50
|
end
|
data/lib/email_address/config.rb
CHANGED
@@ -17,6 +17,11 @@ module EmailAddress
|
|
17
17
|
# the SHA1 Digest, making it unique to your application so it can't easily be
|
18
18
|
# discovered by comparing against a known list of email/sha1 pairs.
|
19
19
|
#
|
20
|
+
# * sha256_secret ""
|
21
|
+
# This application-level secret is appended to the email_address to compute
|
22
|
+
# the SHA256 Digest, making it unique to your application so it can't easily be
|
23
|
+
# discovered by comparing against a known list of email/sha256 pairs.
|
24
|
+
#
|
20
25
|
# For local part configuration:
|
21
26
|
# * local_downcase: true
|
22
27
|
# Downcase the local part. You probably want this for uniqueness.
|
@@ -104,6 +109,7 @@ module EmailAddress
|
|
104
109
|
dns_lookup: :mx, # :mx, :a, :off
|
105
110
|
dns_timeout: nil,
|
106
111
|
sha1_secret: "",
|
112
|
+
sha256_secret: "",
|
107
113
|
munge_string: "*****",
|
108
114
|
|
109
115
|
local_downcase: true,
|
@@ -143,6 +149,7 @@ module EmailAddress
|
|
143
149
|
},
|
144
150
|
msn: {
|
145
151
|
host_match: %w[msn. hotmail. outlook. live.],
|
152
|
+
exchanger_match: %w[outlook.com],
|
146
153
|
mailbox_validator: ->(m, t) { m =~ /\A\w[\-\w]*(?:\.[\-\w]+)*\z/i }
|
147
154
|
},
|
148
155
|
yahoo: {
|
@@ -187,9 +194,14 @@ module EmailAddress
|
|
187
194
|
# Customize your own error message text.
|
188
195
|
def self.error_messages(hash = {}, locale = "en", *extra)
|
189
196
|
hash = extra.first if extra.first.is_a? Hash
|
190
|
-
|
197
|
+
|
198
|
+
@errors[locale] ||= {}
|
199
|
+
@errors[locale]["email_address"] ||= {}
|
200
|
+
|
201
|
+
unless hash.nil? || hash.empty?
|
191
202
|
@errors[locale]["email_address"] = @errors[locale]["email_address"].merge(hash)
|
192
203
|
end
|
204
|
+
|
193
205
|
@errors[locale]["email_address"]
|
194
206
|
end
|
195
207
|
|
@@ -200,7 +212,7 @@ module EmailAddress
|
|
200
212
|
end
|
201
213
|
|
202
214
|
def initialize(overrides = {})
|
203
|
-
@config =
|
215
|
+
@config = Config.all_settings(overrides)
|
204
216
|
end
|
205
217
|
|
206
218
|
def []=(setting, value)
|
@@ -29,20 +29,22 @@
|
|
29
29
|
# user.canonical_email #=> "patsmith@gmail.com"
|
30
30
|
################################################################################
|
31
31
|
|
32
|
-
|
32
|
+
module EmailAddress
|
33
|
+
class EmailAddressType < ActiveRecord::Type::Value
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
# From user input, setter
|
36
|
+
def cast(value)
|
37
|
+
super(Address.new(value).normal)
|
38
|
+
end
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
40
|
+
# From a database value
|
41
|
+
def deserialize(value)
|
42
|
+
value && Address.new(value).normal
|
43
|
+
end
|
44
|
+
|
45
|
+
# To a database value (string)
|
46
|
+
def serialize(value)
|
47
|
+
value && Address.new(value).normal
|
48
|
+
end
|
47
49
|
end
|
48
50
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "resolv"
|
4
|
-
require "netaddr"
|
5
4
|
require "socket"
|
6
5
|
|
7
6
|
module EmailAddress
|
@@ -24,7 +23,7 @@ module EmailAddress
|
|
24
23
|
|
25
24
|
def initialize(host, config = {})
|
26
25
|
@host = host
|
27
|
-
@config = config.is_a?(Hash) ?
|
26
|
+
@config = config.is_a?(Hash) ? Config.new(config) : config
|
28
27
|
@dns_disabled = @config[:host_validation] == :syntax || @config[:dns_lookup] == :off
|
29
28
|
end
|
30
29
|
|
@@ -38,7 +37,7 @@ module EmailAddress
|
|
38
37
|
# Returns the provider name based on the MX-er host names, or nil if not matched
|
39
38
|
def provider
|
40
39
|
return @provider if defined? @provider
|
41
|
-
|
40
|
+
Config.providers.each do |provider, config|
|
42
41
|
if config[:exchanger_match] && matches?(config[:exchanger_match])
|
43
42
|
return @provider = provider
|
44
43
|
end
|
@@ -58,8 +57,8 @@ module EmailAddress
|
|
58
57
|
|
59
58
|
ress = begin
|
60
59
|
dns.getresources(@host, Resolv::DNS::Resource::IN::MX)
|
61
|
-
|
62
|
-
|
60
|
+
rescue Resolv::ResolvTimeout
|
61
|
+
[]
|
63
62
|
end
|
64
63
|
|
65
64
|
records = ress.map { |r|
|
@@ -76,7 +75,7 @@ module EmailAddress
|
|
76
75
|
|
77
76
|
# Returns Array of domain names for the MX'ers, used to determine the Provider
|
78
77
|
def domains
|
79
|
-
@_domains ||= mxers.map { |m|
|
78
|
+
@_domains ||= mxers.map { |m| Host.new(m.first).domain_name }.sort.uniq
|
80
79
|
end
|
81
80
|
|
82
81
|
# Returns an array of MX IP address (String) for the given email domain
|
@@ -105,22 +104,9 @@ module EmailAddress
|
|
105
104
|
|
106
105
|
# Given a cidr (ip/bits) and ip address, returns true on match. Caches cidr object.
|
107
106
|
def in_cidr?(cidr)
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
next unless ip.include?(":")
|
112
|
-
rel = c.rel NetAddr::IPv6Net.parse(ip)
|
113
|
-
!rel.nil? && rel >= 0
|
114
|
-
end
|
115
|
-
elsif cidr.include?(".")
|
116
|
-
c = NetAddr::IPv4Net.parse(cidr)
|
117
|
-
return true if mx_ips.find do |ip|
|
118
|
-
next if ip.include?(":")
|
119
|
-
rel = c.rel NetAddr::IPv4Net.parse(ip)
|
120
|
-
!rel.nil? && rel >= 0
|
121
|
-
end
|
122
|
-
end
|
123
|
-
false
|
107
|
+
net = IPAddr.new(cidr)
|
108
|
+
found = mx_ips.detect { |ip| net.include?(IPAddr.new(ip)) }
|
109
|
+
!!found
|
124
110
|
end
|
125
111
|
end
|
126
112
|
end
|
data/lib/email_address/host.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require "simpleidn"
|
2
2
|
require "resolv"
|
3
|
-
require "netaddr"
|
4
3
|
require "net/smtp"
|
5
4
|
|
6
5
|
module EmailAddress
|
@@ -34,15 +33,15 @@ module EmailAddress
|
|
34
33
|
attr_reader :host_name
|
35
34
|
attr_accessor :dns_name, :domain_name, :registration_name,
|
36
35
|
:tld, :tld2, :subdomains, :ip_address, :config, :provider,
|
37
|
-
:comment, :error_message, :reason
|
36
|
+
:comment, :error_message, :reason, :locale
|
38
37
|
MAX_HOST_LENGTH = 255
|
39
38
|
|
40
39
|
# Sometimes, you just need a Regexp...
|
41
|
-
DNS_HOST_REGEX = / [\p{L}\p{N}]+ (?: (?:
|
40
|
+
DNS_HOST_REGEX = / [\p{L}\p{N}]+ (?: (?: -{1,2} | \.) [\p{L}\p{N}]+ )*/x
|
42
41
|
|
43
42
|
# The IPv4 and IPv6 were lifted from Resolv::IPv?::Regex and tweaked to not
|
44
43
|
# \A...\z anchor at the edges.
|
45
|
-
|
44
|
+
IPV6_HOST_REGEX = /\[IPv6:
|
46
45
|
(?: (?:(?x-mi:
|
47
46
|
(?:[0-9A-Fa-f]{1,4}:){7}
|
48
47
|
[0-9A-Fa-f]{1,4}
|
@@ -61,7 +60,7 @@ module EmailAddress
|
|
61
60
|
(?: \d+)\.(?: \d+)\.(?: \d+)\.(?: \d+)
|
62
61
|
)))\]/ix
|
63
62
|
|
64
|
-
|
63
|
+
IPV4_HOST_REGEX = /\[((?x-mi:0
|
65
64
|
|1(?:[0-9][0-9]?)?
|
66
65
|
|2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
|
67
66
|
|[3-9][0-9]?))\.((?x-mi:0
|
@@ -80,14 +79,15 @@ module EmailAddress
|
|
80
79
|
|
81
80
|
# Matches Host forms: DNS name, IPv4, or IPv6 formats
|
82
81
|
STANDARD_HOST_REGEX = /\A (?: #{DNS_HOST_REGEX}
|
83
|
-
| #{
|
82
|
+
| #{IPV4_HOST_REGEX} | #{IPV6_HOST_REGEX}) \z/ix
|
84
83
|
|
85
84
|
# host name -
|
86
85
|
# * host type - :email for an email host, :mx for exchanger host
|
87
|
-
def initialize(host_name, config = {})
|
86
|
+
def initialize(host_name, config = {}, locale = "en")
|
88
87
|
@original = host_name ||= ""
|
88
|
+
@locale = locale
|
89
89
|
config[:host_type] ||= :email
|
90
|
-
@config = config.is_a?(Hash) ?
|
90
|
+
@config = config.is_a?(Hash) ? Config.new(config) : config
|
91
91
|
@error = @error_message = nil
|
92
92
|
parse(host_name)
|
93
93
|
end
|
@@ -104,7 +104,7 @@ module EmailAddress
|
|
104
104
|
dns_name
|
105
105
|
end
|
106
106
|
end
|
107
|
-
|
107
|
+
alias_method :to_s, :name
|
108
108
|
|
109
109
|
# The canonical host name is the simplified, DNS host name
|
110
110
|
def canonical
|
@@ -149,7 +149,7 @@ module EmailAddress
|
|
149
149
|
if @config[:host_remove_spaces]
|
150
150
|
@host_name = @host_name.delete(" ")
|
151
151
|
end
|
152
|
-
@dns_name = if /[^[:ascii:]]/.match(host_name)
|
152
|
+
@dns_name = if /[^[:ascii:]]/.match?(host_name)
|
153
153
|
::SimpleIDN.to_ascii(host_name)
|
154
154
|
else
|
155
155
|
host_name
|
@@ -209,7 +209,7 @@ module EmailAddress
|
|
209
209
|
def find_provider # :nodoc:
|
210
210
|
return provider if provider
|
211
211
|
|
212
|
-
|
212
|
+
Config.providers.each do |provider, config|
|
213
213
|
if config[:host_match] && matches?(config[:host_match])
|
214
214
|
return set_provider(provider, config)
|
215
215
|
end
|
@@ -217,12 +217,6 @@ module EmailAddress
|
|
217
217
|
|
218
218
|
return set_provider(:default) unless dns_enabled?
|
219
219
|
|
220
|
-
provider = exchangers.provider
|
221
|
-
if provider != :default
|
222
|
-
set_provider(provider,
|
223
|
-
EmailAddress::Config.provider(provider))
|
224
|
-
end
|
225
|
-
|
226
220
|
self.provider ||= set_provider(:default)
|
227
221
|
end
|
228
222
|
|
@@ -235,7 +229,11 @@ module EmailAddress
|
|
235
229
|
def parts
|
236
230
|
{host_name: host_name, dns_name: dns_name, subdomain: subdomains,
|
237
231
|
registration_name: registration_name, domain_name: domain_name,
|
238
|
-
tld2: tld2, tld: tld, ip_address: ip_address
|
232
|
+
tld2: tld2, tld: tld, ip_address: ip_address}
|
233
|
+
end
|
234
|
+
|
235
|
+
def hosted_provider
|
236
|
+
Exchanger.cached(dns_name).provider
|
239
237
|
end
|
240
238
|
|
241
239
|
############################################################################
|
@@ -248,7 +246,7 @@ module EmailAddress
|
|
248
246
|
end
|
249
247
|
|
250
248
|
def ip?
|
251
|
-
ip_address
|
249
|
+
!!ip_address
|
252
250
|
end
|
253
251
|
|
254
252
|
def ipv4?
|
@@ -292,7 +290,7 @@ module EmailAddress
|
|
292
290
|
# Does "sub.example.com" match ".com" and ".example.com" top level names?
|
293
291
|
# Matches TLD (uk) or TLD2 (co.uk)
|
294
292
|
def tld_matches?(rule)
|
295
|
-
rule.match(/\A\.(.+)\z/) && ($1 == tld || $1 == tld2) ? true : false
|
293
|
+
rule.match(/\A\.(.+)\z/) && ($1 == tld || $1 == tld2) # ? true : false
|
296
294
|
end
|
297
295
|
|
298
296
|
def provider_matches?(rule)
|
@@ -312,13 +310,8 @@ module EmailAddress
|
|
312
310
|
# the passed CIDR string ("10.9.8.0/24" or "2001:..../64")
|
313
311
|
def ip_matches?(cidr)
|
314
312
|
return false unless ip_address
|
315
|
-
|
316
|
-
|
317
|
-
return cidr if NetAddr::IPv6Net.parse(cidr).contains(NetAddr::IPv6.parse(ip_address))
|
318
|
-
elsif cidr.include?(".") && ip_address.include?(".")
|
319
|
-
return cidr if NetAddr::IPv4Net.parse(cidr).contains(NetAddr::IPv4.parse(ip_address))
|
320
|
-
end
|
321
|
-
false
|
313
|
+
net = IPAddr.new(cidr)
|
314
|
+
net.include?(IPAddr.new(ip_address))
|
322
315
|
end
|
323
316
|
|
324
317
|
############################################################################
|
@@ -335,16 +328,16 @@ module EmailAddress
|
|
335
328
|
# Returns: [official_hostname, alias_hostnames, address_family, *address_list]
|
336
329
|
def dns_a_record
|
337
330
|
@_dns_a_record = "0.0.0.0" if @config[:dns_lookup] == :off
|
338
|
-
@_dns_a_record ||=
|
331
|
+
@_dns_a_record ||= Addrinfo.getaddrinfo(dns_name, 80) # Port 80 for A rec, 25 for MX
|
339
332
|
rescue SocketError # not found, but could also mean network not work
|
340
333
|
@_dns_a_record ||= []
|
341
334
|
end
|
342
335
|
|
343
|
-
# Returns an array of
|
336
|
+
# Returns an array of Exchanger hosts configured in DNS.
|
344
337
|
# The array will be empty if none are configured.
|
345
338
|
def exchangers
|
346
339
|
# return nil if @config[:host_type] != :email || !self.dns_enabled?
|
347
|
-
@_exchangers ||=
|
340
|
+
@_exchangers ||= Exchanger.cached(dns_name, @config)
|
348
341
|
end
|
349
342
|
|
350
343
|
# Returns a DNS TXT Record
|
@@ -355,8 +348,8 @@ module EmailAddress
|
|
355
348
|
records = begin
|
356
349
|
dns.getresources(alternate_host || dns_name,
|
357
350
|
Resolv::DNS::Resource::IN::TXT)
|
358
|
-
|
359
|
-
|
351
|
+
rescue Resolv::ResolvTimeout
|
352
|
+
[]
|
360
353
|
end
|
361
354
|
|
362
355
|
records.empty? ? nil : records.map(&:data).join(" ")
|
@@ -461,21 +454,9 @@ module EmailAddress
|
|
461
454
|
end
|
462
455
|
|
463
456
|
def localhost?
|
464
|
-
if
|
465
|
-
|
466
|
-
|
467
|
-
NetAddr::IPv6Net.parse("" + "::1").rel(
|
468
|
-
NetAddr::IPv6Net.parse(ip_address)
|
469
|
-
)
|
470
|
-
else
|
471
|
-
NetAddr::IPv4Net.parse("" + "127.0.0.0/8").rel(
|
472
|
-
NetAddr::IPv4Net.parse(ip_address)
|
473
|
-
)
|
474
|
-
end
|
475
|
-
!rel.nil? && rel >= 0
|
476
|
-
else
|
477
|
-
host_name == "localhost"
|
478
|
-
end
|
457
|
+
return true if host_name == "localhost"
|
458
|
+
return false unless ip_address
|
459
|
+
IPAddr.new(ip_address).loopback?
|
479
460
|
end
|
480
461
|
|
481
462
|
# Connects to host to test it can receive email. This should NOT be performed
|
@@ -499,7 +480,7 @@ module EmailAddress
|
|
499
480
|
def set_error(err, reason = nil)
|
500
481
|
@error = err
|
501
482
|
@reason = reason
|
502
|
-
@error_message =
|
483
|
+
@error_message = Config.error_message(err, locale)
|
503
484
|
false
|
504
485
|
end
|
505
486
|
|
data/lib/email_address/local.rb
CHANGED
@@ -67,50 +67,51 @@ module EmailAddress
|
|
67
67
|
# [CFWS]
|
68
68
|
############################################################################
|
69
69
|
class Local
|
70
|
-
attr_reader
|
70
|
+
attr_reader :local
|
71
71
|
attr_accessor :mailbox, :comment, :tag, :config, :original
|
72
|
-
attr_accessor :syntax
|
72
|
+
attr_accessor :syntax, :locale
|
73
73
|
|
74
74
|
# RFC-2142: MAILBOX NAMES FOR COMMON SERVICES, ROLES AND FUNCTIONS
|
75
|
-
BUSINESS_MAILBOXES = %w
|
76
|
-
NETWORK_MAILBOXES
|
77
|
-
SERVICE_MAILBOXES
|
78
|
-
SYSTEM_MAILBOXES
|
79
|
-
ROLE_MAILBOXES
|
80
|
-
SPECIAL_MAILBOXES
|
81
|
-
|
82
|
-
STANDARD_MAX_SIZE
|
75
|
+
BUSINESS_MAILBOXES = %w[info marketing sales support]
|
76
|
+
NETWORK_MAILBOXES = %w[abuse noc security]
|
77
|
+
SERVICE_MAILBOXES = %w[postmaster hostmaster usenet news webmaster www uucp ftp]
|
78
|
+
SYSTEM_MAILBOXES = %w[help mailer-daemon root] # Not from RFC-2142
|
79
|
+
ROLE_MAILBOXES = %w[staff office orders billing careers jobs] # Not from RFC-2142
|
80
|
+
SPECIAL_MAILBOXES = BUSINESS_MAILBOXES + NETWORK_MAILBOXES + SERVICE_MAILBOXES +
|
81
|
+
SYSTEM_MAILBOXES + ROLE_MAILBOXES
|
82
|
+
STANDARD_MAX_SIZE = 64
|
83
83
|
|
84
84
|
# Conventional : word([.-+'_]word)*
|
85
|
-
CONVENTIONAL_MAILBOX_REGEX
|
86
|
-
CONVENTIONAL_MAILBOX_WITHIN = /[\p{L}\p{N}_]+ (?: [
|
85
|
+
CONVENTIONAL_MAILBOX_REGEX = /\A [\p{L}\p{N}_]+ (?: [.\-+'_] [\p{L}\p{N}_]+ )* \z/x
|
86
|
+
CONVENTIONAL_MAILBOX_WITHIN = /[\p{L}\p{N}_]+ (?: [.\-+'_] [\p{L}\p{N}_]+ )*/x
|
87
87
|
|
88
88
|
# Relaxed: same characters, relaxed order
|
89
|
-
RELAXED_MAILBOX_WITHIN = /[\p{L}\p{N}_]+ (?: [
|
90
|
-
RELAXED_MAILBOX_REGEX = /\A [\p{L}\p{N}_]+ (?: [
|
89
|
+
RELAXED_MAILBOX_WITHIN = /[\p{L}\p{N}_]+ (?: [.\-+'_]+ [\p{L}\p{N}_]+ )*/x
|
90
|
+
RELAXED_MAILBOX_REGEX = /\A [\p{L}\p{N}_]+ (?: [.\-+'_]+ [\p{L}\p{N}_]+ )* \z/x
|
91
91
|
|
92
92
|
# RFC5322 Token: token."token".token (dot-separated tokens)
|
93
93
|
# Quoted Token can also have: SPACE \" \\ ( ) , : ; < > @ [ \ ] .
|
94
94
|
STANDARD_LOCAL_WITHIN = /
|
95
|
-
(?: [\p{L}\p{N}
|
96
|
-
|
|
97
|
-
(?: \. (?: [\p{L}\p{N}
|
98
|
-
|
|
95
|
+
(?: [\p{L}\p{N}!\#$%&'*+\-\/=?\^_`{|}~()]+
|
96
|
+
| " (?: \\[" \\] | [\x20-\x21\x23-\x2F\x3A-\x40\x5B\x5D-\x60\x7B-\x7E\p{L}\p{N}] )+ " )
|
97
|
+
(?: \. (?: [\p{L}\p{N}!\#$%&'*+\-\/=?\^_`{|}~()]+
|
98
|
+
| " (?: \\[" \\] | [\x20-\x21\x23-\x2F\x3A-\x40\x5B\x5D-\x60\x7B-\x7E\p{L}\p{N}] )+ " ) )* /x
|
99
99
|
|
100
100
|
STANDARD_LOCAL_REGEX = /\A #{STANDARD_LOCAL_WITHIN} \z/x
|
101
101
|
|
102
102
|
REDACTED_REGEX = /\A \{ [0-9a-f]{40} \} \z/x # {sha1}
|
103
103
|
|
104
|
-
CONVENTIONAL_TAG_REGEX
|
105
|
-
%r
|
106
|
-
RELAXED_TAG_REGEX
|
107
|
-
%r/^([\w
|
104
|
+
CONVENTIONAL_TAG_REGEX = # AZaz09_!'+-/=
|
105
|
+
%r{^([\w!'+\-/=.]+)$}i
|
106
|
+
RELAXED_TAG_REGEX = # AZaz09_!#$%&'*+-/=?^`{|}~
|
107
|
+
%r/^([\w.!\#$%&'*+\-\/=?\^`{|}~]+)$/i
|
108
108
|
|
109
|
-
def initialize(local, config={}, host=nil)
|
110
|
-
@config = config.is_a?(Hash) ?
|
111
|
-
self.local
|
112
|
-
@host
|
113
|
-
@
|
109
|
+
def initialize(local, config = {}, host = nil, locale = "en")
|
110
|
+
@config = config.is_a?(Hash) ? Config.new(config) : config
|
111
|
+
self.local = local
|
112
|
+
@host = host
|
113
|
+
@locale = locale
|
114
|
+
@error = @error_message = nil
|
114
115
|
end
|
115
116
|
|
116
117
|
def local=(raw)
|
@@ -121,23 +122,23 @@ module EmailAddress
|
|
121
122
|
if @config[:local_parse].is_a?(Proc)
|
122
123
|
self.mailbox, self.tag, self.comment = @config[:local_parse].call(raw)
|
123
124
|
else
|
124
|
-
self.mailbox, self.tag, self.comment =
|
125
|
+
self.mailbox, self.tag, self.comment = parse(raw)
|
125
126
|
end
|
126
127
|
|
127
128
|
self.format
|
128
129
|
end
|
129
130
|
|
130
131
|
def parse(raw)
|
131
|
-
if raw =~ /\A
|
132
|
+
if raw =~ /\A"(.*)"\z/ # Quoted
|
132
133
|
raw = $1
|
133
134
|
raw = raw.gsub(/\\(.)/, '\1') # Unescape
|
134
135
|
elsif @config[:local_fix] && @config[:local_format] != :standard
|
135
|
-
raw = raw.
|
136
|
-
raw = raw.
|
137
|
-
#raw.gsub!(/([^\p{L}\p{N}]{2,10})/) {|s| s[0] } # Stutter punctuation typo
|
136
|
+
raw = raw.delete(" ")
|
137
|
+
raw = raw.tr(",", ".")
|
138
|
+
# raw.gsub!(/([^\p{L}\p{N}]{2,10})/) {|s| s[0] } # Stutter punctuation typo
|
138
139
|
end
|
139
|
-
raw, comment =
|
140
|
-
mailbox, tag =
|
140
|
+
raw, comment = parse_comment(raw)
|
141
|
+
mailbox, tag = parse_tag(raw)
|
141
142
|
mailbox ||= ""
|
142
143
|
[mailbox, tag, comment]
|
143
144
|
end
|
@@ -156,28 +157,28 @@ module EmailAddress
|
|
156
157
|
end
|
157
158
|
|
158
159
|
def parse_tag(raw)
|
159
|
-
separator = @config[:tag_separator] ||=
|
160
|
+
separator = @config[:tag_separator] ||= "+"
|
160
161
|
raw.split(separator, 2)
|
161
162
|
end
|
162
163
|
|
163
164
|
# True if the the value contains only Latin characters (7-bit ASCII)
|
164
165
|
def ascii?
|
165
|
-
!
|
166
|
+
!unicode?
|
166
167
|
end
|
167
168
|
|
168
169
|
# True if the the value contains non-Latin Unicde characters
|
169
170
|
def unicode?
|
170
|
-
|
171
|
+
/[^\p{InBasicLatin}]/.match?(local)
|
171
172
|
end
|
172
173
|
|
173
174
|
# Returns true if the value matches the Redacted format
|
174
175
|
def redacted?
|
175
|
-
|
176
|
+
REDACTED_REGEX.match?(local)
|
176
177
|
end
|
177
178
|
|
178
179
|
# Returns true if the value matches the Redacted format
|
179
180
|
def self.redacted?(local)
|
180
|
-
|
181
|
+
REDACTED_REGEX.match?(local)
|
181
182
|
end
|
182
183
|
|
183
184
|
# Is the address for a common system or business role account?
|
@@ -190,81 +191,80 @@ module EmailAddress
|
|
190
191
|
end
|
191
192
|
|
192
193
|
# Builds the local string according to configurations
|
193
|
-
def format(form
|
194
|
+
def format(form = @config[:local_format] || :conventional)
|
194
195
|
if @config[:local_format].is_a?(Proc)
|
195
196
|
@config[:local_format].call(self)
|
196
197
|
elsif form == :conventional
|
197
|
-
|
198
|
+
conventional
|
198
199
|
elsif form == :canonical
|
199
|
-
|
200
|
+
canonical
|
200
201
|
elsif form == :relaxed
|
201
|
-
|
202
|
+
relax
|
202
203
|
elsif form == :standard
|
203
|
-
|
204
|
+
standard
|
204
205
|
end
|
205
206
|
end
|
206
207
|
|
207
208
|
# Returns a conventional form of the address
|
208
209
|
def conventional
|
209
|
-
if
|
210
|
-
[
|
210
|
+
if tag
|
211
|
+
[mailbox, tag].join(@config[:tag_separator])
|
211
212
|
else
|
212
|
-
|
213
|
+
mailbox
|
213
214
|
end
|
214
215
|
end
|
215
216
|
|
216
217
|
# Returns a canonical form of the address
|
217
218
|
def canonical
|
218
219
|
if @config[:mailbox_canonical]
|
219
|
-
@config[:mailbox_canonical].call(
|
220
|
+
@config[:mailbox_canonical].call(mailbox)
|
220
221
|
else
|
221
|
-
|
222
|
+
mailbox.downcase
|
222
223
|
end
|
223
224
|
end
|
224
225
|
|
225
226
|
# Relaxed format: mailbox and tag, no comment, no extended character set
|
226
227
|
def relax
|
227
|
-
form =
|
228
|
-
form += @config[:tag_separator] +
|
229
|
-
form
|
230
|
-
form
|
228
|
+
form = mailbox
|
229
|
+
form += @config[:tag_separator] + tag if tag
|
230
|
+
form.gsub(/[ "(),:<>@\[\]\\]/, "")
|
231
231
|
end
|
232
232
|
|
233
233
|
# Returns a normalized version of the standard address parts.
|
234
234
|
def standard
|
235
|
-
form =
|
236
|
-
form += @config[:tag_separator] +
|
237
|
-
form += "(" +
|
238
|
-
form = form.gsub(/([
|
239
|
-
if
|
240
|
-
form = %
|
235
|
+
form = mailbox
|
236
|
+
form += @config[:tag_separator] + tag if tag
|
237
|
+
form += "(" + comment + ")" if comment
|
238
|
+
form = form.gsub(/([\\"])/, '\\\1') # Escape \ and "
|
239
|
+
if /[ "(),:<>@\[\\\]]/.match?(form) # Space and "(),:;<>@[\]
|
240
|
+
form = %("#{form}")
|
241
241
|
end
|
242
242
|
form
|
243
243
|
end
|
244
244
|
|
245
245
|
# Sets the part to be the conventional form
|
246
246
|
def conventional!
|
247
|
-
self.local =
|
247
|
+
self.local = conventional
|
248
248
|
end
|
249
249
|
|
250
250
|
# Sets the part to be the canonical form
|
251
251
|
def canonical!
|
252
|
-
self.local =
|
252
|
+
self.local = canonical
|
253
253
|
end
|
254
254
|
|
255
255
|
# Dropps unusual parts of Standard form to form a relaxed version.
|
256
256
|
def relax!
|
257
|
-
self.local =
|
257
|
+
self.local = relax
|
258
258
|
end
|
259
259
|
|
260
260
|
# Returns the munged form of the address, like "ma*****"
|
261
261
|
def munge
|
262
|
-
|
262
|
+
to_s.sub(/\A(.{1,2}).*/) { |m| $1 + @config[:munge_string] }
|
263
263
|
end
|
264
264
|
|
265
265
|
# Mailbox with trailing numbers removed
|
266
266
|
def root_name
|
267
|
-
|
267
|
+
mailbox =~ /\A(.+?)\d+\z/ ? $1 : mailbox
|
268
268
|
end
|
269
269
|
|
270
270
|
############################################################################
|
@@ -272,19 +272,19 @@ module EmailAddress
|
|
272
272
|
############################################################################
|
273
273
|
|
274
274
|
# True if the part is valid according to the configurations
|
275
|
-
def valid?(format
|
275
|
+
def valid?(format = @config[:local_format] || :conventional)
|
276
276
|
if @config[:mailbox_validator].is_a?(Proc)
|
277
|
-
@config[:mailbox_validator].call(
|
277
|
+
@config[:mailbox_validator].call(mailbox, tag)
|
278
278
|
elsif format.is_a?(Proc)
|
279
279
|
format.call(self)
|
280
280
|
elsif format == :conventional
|
281
|
-
|
281
|
+
conventional?
|
282
282
|
elsif format == :relaxed
|
283
|
-
|
283
|
+
relaxed?
|
284
284
|
elsif format == :redacted
|
285
|
-
|
285
|
+
redacted?
|
286
286
|
elsif format == :standard
|
287
|
-
|
287
|
+
standard?
|
288
288
|
elsif format == :none
|
289
289
|
true
|
290
290
|
else
|
@@ -295,13 +295,13 @@ module EmailAddress
|
|
295
295
|
# Returns the format of the address
|
296
296
|
def format?
|
297
297
|
# if :custom
|
298
|
-
if
|
298
|
+
if conventional?
|
299
299
|
:conventional
|
300
|
-
elsif
|
300
|
+
elsif relaxed?
|
301
301
|
:relax
|
302
|
-
elsif
|
302
|
+
elsif redacted?
|
303
303
|
:redacted
|
304
|
-
elsif
|
304
|
+
elsif standard?
|
305
305
|
:standard
|
306
306
|
else
|
307
307
|
:invalid
|
@@ -309,38 +309,38 @@ module EmailAddress
|
|
309
309
|
end
|
310
310
|
|
311
311
|
def valid_size?
|
312
|
-
return set_error(:local_size_long) if
|
313
|
-
if @host
|
312
|
+
return set_error(:local_size_long) if local.size > STANDARD_MAX_SIZE
|
313
|
+
if @host&.hosted_service?
|
314
314
|
return false if @config[:local_private_size] && !valid_size_checks(@config[:local_private_size])
|
315
|
-
|
316
|
-
return false
|
315
|
+
elsif @config[:local_size] && !valid_size_checks(@config[:local_size])
|
316
|
+
return false
|
317
317
|
end
|
318
318
|
return false if @config[:mailbox_size] && !valid_size_checks(@config[:mailbox_size])
|
319
319
|
true
|
320
320
|
end
|
321
321
|
|
322
322
|
def valid_size_checks(range)
|
323
|
-
return set_error(:local_size_short) if
|
324
|
-
return set_error(:local_size_long)
|
323
|
+
return set_error(:local_size_short) if mailbox.size < range.first
|
324
|
+
return set_error(:local_size_long) if mailbox.size > range.last
|
325
325
|
true
|
326
326
|
end
|
327
327
|
|
328
|
-
def valid_encoding?(enc
|
329
|
-
return false if enc == :ascii &&
|
328
|
+
def valid_encoding?(enc = @config[:local_encoding] || :ascii)
|
329
|
+
return false if enc == :ascii && unicode?
|
330
330
|
true
|
331
331
|
end
|
332
332
|
|
333
333
|
# True if the part matches the conventional format
|
334
334
|
def conventional?
|
335
335
|
self.syntax = :invalid
|
336
|
-
if
|
337
|
-
return false unless
|
338
|
-
|
336
|
+
if tag
|
337
|
+
return false unless mailbox =~ CONVENTIONAL_MAILBOX_REGEX &&
|
338
|
+
tag =~ CONVENTIONAL_TAG_REGEX
|
339
339
|
else
|
340
|
-
return false unless
|
340
|
+
return false unless CONVENTIONAL_MAILBOX_REGEX.match?(local)
|
341
341
|
end
|
342
|
-
|
343
|
-
|
342
|
+
valid_size? or return false
|
343
|
+
valid_encoding? or return false
|
344
344
|
self.syntax = :conventional
|
345
345
|
true
|
346
346
|
end
|
@@ -348,12 +348,12 @@ module EmailAddress
|
|
348
348
|
# Relaxed conventional is not so strict about character order.
|
349
349
|
def relaxed?
|
350
350
|
self.syntax = :invalid
|
351
|
-
|
352
|
-
|
353
|
-
if
|
354
|
-
return false unless
|
355
|
-
|
356
|
-
elsif
|
351
|
+
valid_size? or return false
|
352
|
+
valid_encoding? or return false
|
353
|
+
if tag
|
354
|
+
return false unless mailbox =~ RELAXED_MAILBOX_REGEX &&
|
355
|
+
tag =~ RELAXED_TAG_REGEX
|
356
|
+
elsif RELAXED_MAILBOX_REGEX.match?(local)
|
357
357
|
self.syntax = :relaxed
|
358
358
|
true
|
359
359
|
else
|
@@ -364,9 +364,9 @@ module EmailAddress
|
|
364
364
|
# True if the part matches the RFC standard format
|
365
365
|
def standard?
|
366
366
|
self.syntax = :invalid
|
367
|
-
|
368
|
-
|
369
|
-
if
|
367
|
+
valid_size? or return false
|
368
|
+
valid_encoding? or return false
|
369
|
+
if STANDARD_LOCAL_REGEX.match?(local)
|
370
370
|
self.syntax = :standard
|
371
371
|
true
|
372
372
|
else
|
@@ -379,26 +379,23 @@ module EmailAddress
|
|
379
379
|
def matches?(*rules)
|
380
380
|
rules.flatten.each do |r|
|
381
381
|
if r =~ /(.+)@\z/
|
382
|
-
return r if File.fnmatch?($1,
|
382
|
+
return r if File.fnmatch?($1, local)
|
383
383
|
end
|
384
384
|
end
|
385
385
|
false
|
386
386
|
end
|
387
387
|
|
388
|
-
def set_error(err, reason=nil)
|
388
|
+
def set_error(err, reason = nil)
|
389
389
|
@error = err
|
390
|
-
@reason= reason
|
391
|
-
@error_message =
|
390
|
+
@reason = reason
|
391
|
+
@error_message = Config.error_message(err, locale)
|
392
392
|
false
|
393
393
|
end
|
394
394
|
|
395
|
-
|
396
|
-
@error_message
|
397
|
-
end
|
395
|
+
attr_reader :error_message
|
398
396
|
|
399
397
|
def error
|
400
|
-
|
398
|
+
valid? ? nil : (@error || :local_invalid)
|
401
399
|
end
|
402
|
-
|
403
400
|
end
|
404
401
|
end
|