email_address 0.1.16 → 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 +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
|