email_address 0.1.13 → 0.1.18
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/.travis.yml +1 -2
- data/README.md +42 -3
- data/email_address.gemspec +1 -0
- data/lib/email_address.rb +17 -18
- data/lib/email_address/active_record_validator.rb +2 -2
- data/lib/email_address/address.rb +94 -100
- data/lib/email_address/canonical_email_address_type.rb +14 -12
- data/lib/email_address/config.rb +69 -47
- data/lib/email_address/email_address_type.rb +15 -13
- data/lib/email_address/exchanger.rb +29 -30
- data/lib/email_address/host.rb +123 -125
- data/lib/email_address/local.rb +7 -7
- data/lib/email_address/version.rb +1 -1
- data/test/email_address/test_address.rb +5 -0
- data/test/email_address/test_host.rb +29 -31
- data/test/test_aliasing.rb +54 -0
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a5ff67ec79e4de2d356293664296391fc8296ad6a6ba08f1d1256e93f7c729c
|
4
|
+
data.tar.gz: 5485014c6f886d9a382c046f9025948eeed6c114accde2957424f1f613d1b51b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 336f61eb9e81ccf8fe859b0d05e5e4d1aaf734e06328169ce919a01fdf30caaebdb2f44e673b52cdde99e3790c413c854e486fd97e4c610b5ee6349c1e3dbe40
|
7
|
+
data.tar.gz: c08f83be388d6fdb646ea46439c0a65543091e401b9c9d38e312a7d2dc34313c3269f9c16916340d090f48ae224a1cd18631153d6478f87e32073f38e319ff28
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -50,7 +50,7 @@ It does not check:
|
|
50
50
|
By default, MX records are required in DNS. MX or "mail exchanger" records
|
51
51
|
tell where to deliver email for the domain. Many domains run their
|
52
52
|
website on one provider (ISP, Heroku, etc.), and email on a different
|
53
|
-
provider (such as
|
53
|
+
provider (such as G Suite). Note that `example.com`, while
|
54
54
|
a valid domain name, does not have MX records.
|
55
55
|
|
56
56
|
```ruby
|
@@ -248,6 +248,16 @@ EmailAddress.normal("HIRO@こんにちは世界.com")
|
|
248
248
|
EmailAddress.normal("hiro@xn--28j2a3ar1pp75ovm7c.com", host_encoding: :unicode)
|
249
249
|
#=> "hiro@こんにちは世界.com"
|
250
250
|
```
|
251
|
+
As of release 0.1.17, exchanger_match is no longer used for host provider
|
252
|
+
determination, which designated the set of rules for that domain.
|
253
|
+
Sometimes, as in Google-hosted domains, the address
|
254
|
+
rules are different, notably the optional dots in mailboxes for gmail.com
|
255
|
+
accounts do not apply to other private domains hosted at google.
|
256
|
+
|
257
|
+
To access the provider service, you can now call:
|
258
|
+
|
259
|
+
EmailAddress.new("user@hosteddomain.com").host.hosted_provider
|
260
|
+
|
251
261
|
|
252
262
|
#### Rails Validator
|
253
263
|
|
@@ -446,14 +456,16 @@ EmailAddress::Config.provider(:github,
|
|
446
456
|
You can override the default error messages as follows:
|
447
457
|
|
448
458
|
```ruby
|
449
|
-
EmailAddress::Config.error_messages(
|
459
|
+
EmailAddress::Config.error_messages({
|
450
460
|
invalid_address: "Invalid Email Address",
|
451
461
|
invalid_mailbox: "Invalid Recipient/Mailbox",
|
452
462
|
invalid_host: "Invalid Host/Domain Name",
|
453
463
|
exceeds_size: "Address too long",
|
454
464
|
not_allowed: "Address is not allowed",
|
455
|
-
incomplete_domain: "Domain name is incomplete")
|
465
|
+
incomplete_domain: "Domain name is incomplete"}, 'en')
|
456
466
|
```
|
467
|
+
Note: Release 0.1.14 fixed setting error messages by locale.
|
468
|
+
Also, it will accept a ruby "collected" hash as before,
|
457
469
|
|
458
470
|
Full translation support would be ideal though.
|
459
471
|
|
@@ -540,6 +552,33 @@ The value is an array of match tokens.
|
|
540
552
|
* host_match: %w(.org example.com hotmail. user*@ sub.*.com)
|
541
553
|
* exchanger_match: %w(google.com 127.0.0.1 10.9.8.0/24 ::1/64)
|
542
554
|
|
555
|
+
### Namespace conflict resolution
|
556
|
+
|
557
|
+
If your application already uses the `EmailAddress` class name,
|
558
|
+
it's possible to create an alias prior to loading your code:
|
559
|
+
|
560
|
+
For a Rails application, you can do this in `config/application.rb`
|
561
|
+
after the `Bundler.require` line, usually:
|
562
|
+
|
563
|
+
```ruby
|
564
|
+
Bundler.require(*Rails.groups)
|
565
|
+
```
|
566
|
+
|
567
|
+
Add these lines immediately after that point:
|
568
|
+
|
569
|
+
```ruby
|
570
|
+
EmailAddressValidator = EmailAddress
|
571
|
+
Object.send(:remove_const, :EmailAddress)
|
572
|
+
```
|
573
|
+
|
574
|
+
Then your application loads with your EmailAddress class. You may
|
575
|
+
then use this gem with `EmailAddressValidator` or whatever name you
|
576
|
+
gave it above:
|
577
|
+
|
578
|
+
```ruby
|
579
|
+
EmailAddressValidator.valid?("clark.kent@gmail.com") # => true
|
580
|
+
```
|
581
|
+
|
543
582
|
## Notes
|
544
583
|
|
545
584
|
#### Internationalization
|
data/email_address.gemspec
CHANGED
@@ -13,6 +13,7 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.homepage = "https://github.com/afair/email_address"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
|
+
#spec.required_ruby_version = ">= 2.3.0"
|
16
17
|
spec.files = `git ls-files`.split($/)
|
17
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
data/lib/email_address.rb
CHANGED
@@ -40,31 +40,30 @@ module EmailAddress
|
|
40
40
|
# secret to use in options[:secret]
|
41
41
|
class << self
|
42
42
|
(%i[valid? error normal redact munge canonical reference base srs] &
|
43
|
-
|
43
|
+
Address.public_instance_methods
|
44
44
|
).each do |proxy_method|
|
45
45
|
define_method(proxy_method) do |*args, &block|
|
46
|
-
|
46
|
+
Address.new(*args).public_send(proxy_method, &block)
|
47
47
|
end
|
48
48
|
end
|
49
|
-
end
|
50
|
-
|
51
49
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
50
|
+
# Creates an instance of this email address.
|
51
|
+
# This is a short-cut to EmailAddress::Address.new
|
52
|
+
def new(email_address, config={})
|
53
|
+
Address.new(email_address, config)
|
54
|
+
end
|
57
55
|
|
58
|
-
|
59
|
-
|
60
|
-
|
56
|
+
def new_redacted(email_address, config={})
|
57
|
+
Address.new(Address.new(email_address, config).redact)
|
58
|
+
end
|
61
59
|
|
62
|
-
|
63
|
-
|
64
|
-
|
60
|
+
def new_canonical(email_address, config={})
|
61
|
+
Address.new(Address.new(email_address, config).canonical, config)
|
62
|
+
end
|
65
63
|
|
66
|
-
|
67
|
-
|
68
|
-
|
64
|
+
# Does the email address match any of the given rules
|
65
|
+
def matches?(email_address, rules, config={})
|
66
|
+
Address.new(email_address, config).matches?(rules)
|
67
|
+
end
|
69
68
|
end
|
70
69
|
end
|
@@ -35,10 +35,10 @@ module EmailAddress
|
|
35
35
|
|
36
36
|
def validate_email(r,f)
|
37
37
|
return if r[f].nil?
|
38
|
-
e =
|
38
|
+
e = Address.new(r[f])
|
39
39
|
unless e.valid?
|
40
40
|
r.errors[f] << (@opt[:message] ||
|
41
|
-
|
41
|
+
Config.error_messages[:invalid_address] ||
|
42
42
|
"Invalid Email Address")
|
43
43
|
end
|
44
44
|
end
|
@@ -1,46 +1,45 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "digest/sha1"
|
4
|
+
require "digest/md5"
|
5
5
|
|
6
6
|
module EmailAddress
|
7
7
|
# Implements the Email Address container, which hold the Local
|
8
|
-
# (EmailAddress::Local) and Host (
|
8
|
+
# (EmailAddress::Local) and Host (EmailAddress::Host) parts.
|
9
9
|
class Address
|
10
10
|
include Comparable
|
11
|
-
include
|
11
|
+
include Rewriter
|
12
12
|
|
13
13
|
attr_accessor :original, :local, :host, :config, :reason
|
14
14
|
|
15
|
-
CONVENTIONAL_REGEX = /\A#{
|
16
|
-
@#{
|
17
|
-
STANDARD_REGEX
|
18
|
-
@#{
|
19
|
-
RELAXED_REGEX
|
20
|
-
@#{
|
15
|
+
CONVENTIONAL_REGEX = /\A#{Local::CONVENTIONAL_MAILBOX_WITHIN}
|
16
|
+
@#{Host::DNS_HOST_REGEX}\z/x
|
17
|
+
STANDARD_REGEX = /\A#{Local::STANDARD_LOCAL_WITHIN}
|
18
|
+
@#{Host::DNS_HOST_REGEX}\z/x
|
19
|
+
RELAXED_REGEX = /\A#{Local::RELAXED_MAILBOX_WITHIN}
|
20
|
+
@#{Host::DNS_HOST_REGEX}\z/x
|
21
21
|
|
22
22
|
# Given an email address of the form "local@hostname", this sets up the
|
23
23
|
# instance, and initializes the address to the "normalized" format of the
|
24
24
|
# address. The original string is available in the #original method.
|
25
|
-
def initialize(email_address, config={})
|
26
|
-
@config
|
27
|
-
@original
|
28
|
-
email_address
|
29
|
-
email_address
|
30
|
-
local, host
|
31
|
-
|
32
|
-
@host
|
33
|
-
@
|
34
|
-
@
|
35
|
-
@error = @error_message = nil
|
25
|
+
def initialize(email_address, config = {})
|
26
|
+
@config = Config.new(config)
|
27
|
+
@original = email_address
|
28
|
+
email_address = (email_address || "").strip
|
29
|
+
email_address = parse_rewritten(email_address) unless config[:skip_rewrite]
|
30
|
+
local, host = Address.split_local_host(email_address)
|
31
|
+
|
32
|
+
@host = Host.new(host, @config)
|
33
|
+
@local = Local.new(local, @config, @host)
|
34
|
+
@error = @error_message = nil
|
36
35
|
end
|
37
36
|
|
38
37
|
# Given an email address, this returns an array of [local, host] parts
|
39
38
|
def self.split_local_host(email)
|
40
|
-
if lh = email.match(/(.+)@(.+)/)
|
41
|
-
lh.to_a[1,2]
|
39
|
+
if (lh = email.match(/(.+)@(.+)/))
|
40
|
+
lh.to_a[1, 2]
|
42
41
|
else
|
43
|
-
[email,
|
42
|
+
[email, ""]
|
44
43
|
end
|
45
44
|
end
|
46
45
|
|
@@ -54,26 +53,26 @@ module EmailAddress
|
|
54
53
|
|
55
54
|
# Everything to the left of the @ in the address, called the local part.
|
56
55
|
def left
|
57
|
-
|
56
|
+
local.to_s
|
58
57
|
end
|
59
58
|
|
60
59
|
# Returns the mailbox portion of the local port, with no tags. Usually, this
|
61
60
|
# can be considered the user account or role account names. Some systems
|
62
61
|
# employ dynamic email addresses which don't have the same meaning.
|
63
62
|
def mailbox
|
64
|
-
|
63
|
+
local.mailbox
|
65
64
|
end
|
66
65
|
|
67
66
|
# Returns the tag part of the local address, or nil if not given.
|
68
67
|
def tag
|
69
|
-
|
68
|
+
local.tag
|
70
69
|
end
|
71
70
|
|
72
71
|
# Retuns any comments parsed from the local part of the email address.
|
73
72
|
# This is retained for inspection after construction, even if it is
|
74
73
|
# removed from the normalized email address.
|
75
74
|
def comment
|
76
|
-
|
75
|
+
local.comment
|
77
76
|
end
|
78
77
|
|
79
78
|
############################################################################
|
@@ -87,8 +86,8 @@ module EmailAddress
|
|
87
86
|
def host_name
|
88
87
|
@host.host_name
|
89
88
|
end
|
90
|
-
alias
|
91
|
-
alias
|
89
|
+
alias right host_name
|
90
|
+
alias hostname host_name
|
92
91
|
|
93
92
|
# Returns the ESP (Email Service Provider) or ISP name derived
|
94
93
|
# using the provider configuration rules.
|
@@ -104,18 +103,18 @@ module EmailAddress
|
|
104
103
|
def normal
|
105
104
|
if !@original
|
106
105
|
@original
|
107
|
-
elsif
|
106
|
+
elsif local.to_s.size == 0
|
108
107
|
""
|
109
|
-
elsif
|
110
|
-
|
108
|
+
elsif host.to_s.size == 0
|
109
|
+
local.to_s
|
111
110
|
else
|
112
|
-
"#{
|
111
|
+
"#{local}@#{host}"
|
113
112
|
end
|
114
113
|
end
|
115
|
-
alias
|
114
|
+
alias to_s normal
|
116
115
|
|
117
116
|
def inspect
|
118
|
-
"
|
117
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} address=\"#{self}\">"
|
119
118
|
end
|
120
119
|
|
121
120
|
# Returns the canonical email address according to the provider
|
@@ -123,56 +122,56 @@ module EmailAddress
|
|
123
122
|
# spaves and comments and tags, and any extraneous part of the address
|
124
123
|
# not considered a unique account by the provider.
|
125
124
|
def canonical
|
126
|
-
c =
|
127
|
-
c += "@" +
|
125
|
+
c = local.canonical
|
126
|
+
c += "@" + host.canonical if host.canonical && host.canonical > " "
|
128
127
|
c
|
129
128
|
end
|
130
129
|
|
131
130
|
# True if the given address is already in it's canonical form.
|
132
131
|
def canonical?
|
133
|
-
|
132
|
+
canonical == to_s
|
134
133
|
end
|
135
134
|
|
136
135
|
# The base address is the mailbox, without tags, and host.
|
137
136
|
def base
|
138
|
-
|
137
|
+
mailbox + "@" + hostname
|
139
138
|
end
|
140
139
|
|
141
140
|
# Returns the redacted form of the address
|
142
141
|
# This format is defined by this libaray, and may change as usage increases.
|
143
142
|
# Takes either :sha1 (default) or :md5 as the argument
|
144
|
-
def redact(digest
|
145
|
-
raise "Unknown Digest type: #{digest}" unless %i
|
146
|
-
return
|
147
|
-
r = %
|
148
|
-
r += "@" +
|
143
|
+
def redact(digest = :sha1)
|
144
|
+
raise "Unknown Digest type: #{digest}" unless %i[sha1 md5].include?(digest)
|
145
|
+
return to_s if local.redacted?
|
146
|
+
r = %({#{send(digest)}})
|
147
|
+
r += "@" + host.to_s if host.to_s && host.to_s > " "
|
149
148
|
r
|
150
149
|
end
|
151
150
|
|
152
151
|
# True if the address is already in the redacted state.
|
153
152
|
def redacted?
|
154
|
-
|
153
|
+
local.redacted?
|
155
154
|
end
|
156
155
|
|
157
156
|
# Returns the munged form of the address, by default "mailbox@domain.tld"
|
158
157
|
# returns "ma*****@do*****".
|
159
158
|
def munge
|
160
|
-
[
|
159
|
+
[local.munge, host.munge].join("@")
|
161
160
|
end
|
162
161
|
|
163
162
|
# Returns and MD5 of the canonical address form. Some cross-system systems
|
164
163
|
# use the email address MD5 instead of the actual address to refer to the
|
165
164
|
# same shared user identity without exposing the actual address when it
|
166
165
|
# is not known in common.
|
167
|
-
def reference(form
|
168
|
-
Digest::MD5.hexdigest(
|
166
|
+
def reference(form = :base)
|
167
|
+
Digest::MD5.hexdigest(send(form))
|
169
168
|
end
|
170
|
-
alias
|
169
|
+
alias md5 reference
|
171
170
|
|
172
171
|
# This returns the SHA1 digest (in a hex string) of the canonical email
|
173
172
|
# address. See #md5 for more background.
|
174
|
-
def sha1(form
|
175
|
-
Digest::SHA1.hexdigest((
|
173
|
+
def sha1(form = :base)
|
174
|
+
Digest::SHA1.hexdigest((send(form) || "") + (@config[:sha1_secret] || ""))
|
176
175
|
end
|
177
176
|
|
178
177
|
#---------------------------------------------------------------------------
|
@@ -182,41 +181,41 @@ module EmailAddress
|
|
182
181
|
# Equal matches the normalized version of each address. Use the Threequal to check
|
183
182
|
# for match on canonical or redacted versions of addresses
|
184
183
|
def ==(other_email)
|
185
|
-
|
184
|
+
to_s == other_email.to_s
|
186
185
|
end
|
187
|
-
alias
|
188
|
-
alias
|
186
|
+
alias eql? ==
|
187
|
+
alias equal? ==
|
189
188
|
|
190
189
|
# Return the <=> or CMP comparison operator result (-1, 0, +1) on the comparison
|
191
190
|
# of this addres with another, using the canonical or redacted forms.
|
192
191
|
def same_as?(other_email)
|
193
192
|
if other_email.is_a?(String)
|
194
|
-
other_email =
|
193
|
+
other_email = Address.new(other_email)
|
195
194
|
end
|
196
195
|
|
197
|
-
|
198
|
-
|
199
|
-
|
196
|
+
canonical == other_email.canonical ||
|
197
|
+
redact == other_email.canonical ||
|
198
|
+
canonical == other_email.redact
|
200
199
|
end
|
201
|
-
alias
|
200
|
+
alias include? same_as?
|
202
201
|
|
203
202
|
# Return the <=> or CMP comparison operator result (-1, 0, +1) on the comparison
|
204
203
|
# of this addres with another, using the normalized form.
|
205
204
|
def <=>(other_email)
|
206
|
-
|
205
|
+
to_s <=> other_email.to_s
|
207
206
|
end
|
208
207
|
|
209
208
|
# Address matches one of these Matcher rule patterns
|
210
209
|
def matches?(*rules)
|
211
210
|
rules.flatten!
|
212
|
-
match
|
213
|
-
match ||=
|
211
|
+
match = local.matches?(rules)
|
212
|
+
match ||= host.matches?(rules)
|
214
213
|
return match if match
|
215
214
|
|
216
215
|
# Does "root@*.com" match "root@example.com" domain name
|
217
216
|
rules.each do |r|
|
218
|
-
if r
|
219
|
-
return r if File.fnmatch?(r,
|
217
|
+
if r.match(/.+@.+/)
|
218
|
+
return r if File.fnmatch?(r, to_s)
|
220
219
|
end
|
221
220
|
end
|
222
221
|
false
|
@@ -228,24 +227,24 @@ module EmailAddress
|
|
228
227
|
|
229
228
|
# Returns true if this address is considered valid according to the format
|
230
229
|
# configured for its provider, It test the normalized form.
|
231
|
-
def valid?(options={})
|
230
|
+
def valid?(options = {})
|
232
231
|
@error = nil
|
233
|
-
unless
|
234
|
-
return set_error
|
232
|
+
unless local.valid?
|
233
|
+
return set_error local.error
|
235
234
|
end
|
236
|
-
unless
|
237
|
-
return set_error
|
235
|
+
unless host.valid?
|
236
|
+
return set_error host.error_message
|
238
237
|
end
|
239
|
-
if @config[:address_size] && !@config[:address_size].include?(
|
238
|
+
if @config[:address_size] && !@config[:address_size].include?(to_s.size)
|
240
239
|
return set_error :exceeds_size
|
241
240
|
end
|
242
241
|
if @config[:address_validation].is_a?(Proc)
|
243
|
-
unless @config[:address_validation].call(
|
242
|
+
unless @config[:address_validation].call(to_s)
|
244
243
|
return set_error :not_allowed
|
245
244
|
end
|
246
245
|
else
|
247
|
-
return false unless
|
248
|
-
return false unless
|
246
|
+
return false unless local.valid?
|
247
|
+
return false unless host.valid?
|
249
248
|
end
|
250
249
|
true
|
251
250
|
end
|
@@ -254,42 +253,37 @@ module EmailAddress
|
|
254
253
|
# as an email address check, but is provided to assist in problem resolution.
|
255
254
|
# If you abuse this, you *could* be blocked by the ESP.
|
256
255
|
def connect
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
256
|
+
smtp = Net::SMTP.new(host_name || ip_address)
|
257
|
+
smtp.start(@config[:smtp_helo_name] || "localhost")
|
258
|
+
smtp.mailfrom @config[:smtp_mail_from] || "postmaster@localhost"
|
259
|
+
smtp.rcptto to_s
|
260
|
+
# p [:connect]
|
261
|
+
smtp.finish
|
262
|
+
true
|
263
|
+
rescue Net::SMTPUnknownError => e
|
264
|
+
set_error(:address_unknown, e.to_s)
|
265
|
+
rescue Net::SMTPFatalError => e
|
266
|
+
set_error(:address_unknown, e.to_s)
|
267
|
+
rescue SocketError => e
|
268
|
+
set_error(:address_unknown, e.to_s)
|
269
|
+
ensure
|
270
|
+
if smtp&.started?
|
263
271
|
smtp.finish
|
264
|
-
true
|
265
|
-
rescue Net::SMTPUnknownError => e
|
266
|
-
set_error(:address_unknown, e.to_s)
|
267
|
-
rescue Net::SMTPFatalError => e
|
268
|
-
set_error(:address_unknown, e.to_s)
|
269
|
-
rescue SocketError => e
|
270
|
-
set_error(:address_unknown, e.to_s)
|
271
|
-
ensure
|
272
|
-
if smtp && smtp.started?
|
273
|
-
smtp.finish
|
274
|
-
end
|
275
|
-
!!@error
|
276
272
|
end
|
273
|
+
!!@error
|
277
274
|
end
|
278
275
|
|
279
|
-
def set_error(err, reason=nil)
|
276
|
+
def set_error(err, reason = nil)
|
280
277
|
@error = err
|
281
|
-
@reason= reason
|
282
|
-
@error_message =
|
278
|
+
@reason = reason
|
279
|
+
@error_message = Config.error_message(err)
|
283
280
|
false
|
284
281
|
end
|
285
282
|
|
286
|
-
|
287
|
-
@error_message
|
288
|
-
end
|
283
|
+
attr_reader :error_message
|
289
284
|
|
290
285
|
def error
|
291
|
-
|
286
|
+
valid? ? nil : @error_message
|
292
287
|
end
|
293
|
-
|
294
288
|
end
|
295
289
|
end
|