email_address 0.1.7 → 0.1.8
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/README.md +153 -114
- data/email_address.gemspec +1 -1
- data/lib/email_address.rb +17 -9
- data/lib/email_address/address.rb +25 -11
- data/lib/email_address/rewriter.rb +145 -0
- data/lib/email_address/version.rb +1 -1
- data/test/email_address/test_address.rb +12 -2
- data/test/email_address/test_rewriter.rb +14 -0
- metadata +10 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f54dab3992bf4e19436dd4771c376b5c4a770b263b6e91704bb8f4b4aa1c33c6
|
4
|
+
data.tar.gz: 64f8de302e904131bcc25be93674d8e976e6f7ee37d6f75b3c4fcaf362054256
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5f284c688160493c8d30170007b797179ed18c38132e3d9d96d7d1afb390f996ac4f4d68dd3d473706c9a7a8be19cbaa2de2cf64b603032408cc7f3a1d60253
|
7
|
+
data.tar.gz: e78d1280da5668f07bd101f6f83df0e48c8f07a93bfdfe23b8a837632b5c62c57f2e19e309999c3b05c39f9c05a387394efd32bfc6f3ac2cdb28f49d0d4d0cad
|
data/README.md
CHANGED
@@ -28,8 +28,10 @@ To quickly validate email addresses, use the valid? and error helpers.
|
|
28
28
|
`valid?` returns a boolean, and `error` returns nil if valid, otherwise
|
29
29
|
a basic error message.
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
```ruby
|
32
|
+
EmailAddress.valid? "allen@google.com" #=> true
|
33
|
+
EmailAddress.error "allen@bad-d0main.com" #=> "Invalid Host/Domain Name"
|
34
|
+
```
|
33
35
|
|
34
36
|
`EmailAddress` deeply validates your email addresses. It checks:
|
35
37
|
|
@@ -48,14 +50,17 @@ website on one provider (ISP, Heroku, etc.), and email on a different
|
|
48
50
|
provider (such as Google Apps). Note that `example.com`, while
|
49
51
|
a valid domain name, does not have MX records.
|
50
52
|
|
51
|
-
|
52
|
-
|
53
|
+
```ruby
|
54
|
+
EmailAddress.valid? "allen@example.com" #=> false
|
55
|
+
EmailAddress.valid? "allen@example.com", host_validation: :syntax #=> true
|
56
|
+
```
|
53
57
|
|
54
58
|
Most mail servers do not yet support Unicode mailboxes, so the default here is ASCII.
|
55
59
|
|
56
|
-
|
57
|
-
|
58
|
-
|
60
|
+
```ruby
|
61
|
+
EmailAddress.error "Pelé@google.com" #=> "Invalid Recipient/Mailbox"
|
62
|
+
EmailAddress.valid? "Pelé@google.com", local_encoding: :unicode #=> true
|
63
|
+
```
|
59
64
|
|
60
65
|
## Background
|
61
66
|
|
@@ -89,15 +94,20 @@ introduces terms to distinguish types of email addresses.
|
|
89
94
|
|
90
95
|
madness!."()<>[]:,;@\\\"!#$%&'*+-/=?^_`{}| ~.a(comment )"@example.org
|
91
96
|
|
97
|
+
* *Base* - A unique mailbox without tags. For gmail, is uses the incoming
|
98
|
+
punctation, essential when building an MD5 or SHA1 to match services
|
99
|
+
like Gravatar, and email address digest interchange.
|
100
|
+
|
92
101
|
* *Canonical* - An unique account address, lower-cased, without the
|
93
102
|
tag, and with irrelevant characters stripped.
|
94
103
|
|
95
104
|
clark.kent+scoops@gmail.com => clarkkent@gmail.com
|
96
105
|
|
97
|
-
* *Reference* - The MD5 of the
|
106
|
+
* *Reference* - The MD5 of the Base format, used to share account
|
98
107
|
references without exposing the private email address directly.
|
99
108
|
|
100
|
-
Clark.Kent+scoops@gmail.com =>
|
109
|
+
Clark.Kent+scoops@gmail.com =>
|
110
|
+
clark.kent@gmail.com => 1429a1dfc797d6e93075fef011c373fb
|
101
111
|
|
102
112
|
* *Redacted* - A form of the email address where it is replaced by
|
103
113
|
a SHA1-based version to remove the original address from the
|
@@ -181,8 +191,10 @@ If you are not using Bundler, you need to install the gem yourself.
|
|
181
191
|
|
182
192
|
Require the gem inside your script.
|
183
193
|
|
184
|
-
|
185
|
-
|
194
|
+
```ruby
|
195
|
+
require 'rubygems'
|
196
|
+
require 'email_address'
|
197
|
+
```
|
186
198
|
|
187
199
|
## Usage
|
188
200
|
|
@@ -192,41 +204,47 @@ instantiate an object to inspect the address.
|
|
192
204
|
These top-level helpers return edited email addresses and validation
|
193
205
|
check.
|
194
206
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
207
|
+
```ruby
|
208
|
+
address = "Clark.Kent+scoops@gmail.com"
|
209
|
+
EmailAddress.valid?(address) #=> true
|
210
|
+
EmailAddress.normal(address) #=> "clark.kent+scoops@gmail.com"
|
211
|
+
EmailAddress.canonical(address) #=> "clarkkent@gmail.com"
|
212
|
+
EmailAddress.reference(address) #=> "c5be3597c391169a5ad2870f9ca51901"
|
213
|
+
EmailAddress.redact(address) #=> "{bea3f3560a757f8142d38d212a931237b218eb5e}@gmail.com"
|
214
|
+
EmailAddress.munge(address) #=> "cl*****@gm*****"
|
215
|
+
EmailAddress.matches?(address, 'google') #=> 'google' (true)
|
216
|
+
EmailAddress.error("#bad@example.com") #=> "Invalid Mailbox"
|
217
|
+
```
|
204
218
|
|
205
219
|
Or you can create an instance of the email address to work with it.
|
206
220
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
221
|
+
```ruby
|
222
|
+
email = EmailAddress.new(address) #=> #<EmailAddress::Address:0x007fe6ee150540 ...>
|
223
|
+
email.normal #=> "clark.kent+scoops@gmail.com"
|
224
|
+
email.canonical #=> "clarkkent@gmail.com"
|
225
|
+
email.original #=> "Clark.Kent+scoops@gmail.com"
|
226
|
+
email.valid? #=> true
|
227
|
+
```
|
212
228
|
|
213
229
|
Here are some other methods that are available.
|
214
230
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
231
|
+
```ruby
|
232
|
+
email.redact #=> "{bea3f3560a757f8142d38d212a931237b218eb5e}@gmail.com"
|
233
|
+
email.sha1 #=> "bea3f3560a757f8142d38d212a931237b218eb5e"
|
234
|
+
email.md5 #=> "c5be3597c391169a5ad2870f9ca51901"
|
235
|
+
email.host_name #=> "gmail.com"
|
236
|
+
email.provider #=> :google
|
237
|
+
email.mailbox #=> "clark.kent"
|
238
|
+
email.tag #=> "scoops"
|
222
239
|
|
223
|
-
|
224
|
-
|
240
|
+
email.host.exchanger.first[:ip] #=> "2a00:1450:400b:c02::1a"
|
241
|
+
email.host.txt_hash #=> {:v=>"spf1", :redirect=>"\_spf.google.com"}
|
225
242
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
243
|
+
EmailAddress.normal("HIRO@こんにちは世界.com")
|
244
|
+
#=> "hiro@xn--28j2a3ar1pp75ovm7c.com"
|
245
|
+
EmailAddress.normal("hiro@xn--28j2a3ar1pp75ovm7c.com", host_encoding: :unicode)
|
246
|
+
#=> "hiro@こんにちは世界.com"
|
247
|
+
```
|
230
248
|
|
231
249
|
#### Rails Validator
|
232
250
|
|
@@ -235,9 +253,11 @@ Specify your email address attributes with `field: :user_email`, or
|
|
235
253
|
`fields: [:email1, :email2]`. If neither is given, it assumes to use the
|
236
254
|
`email` or `email_address` attribute.
|
237
255
|
|
238
|
-
|
239
|
-
|
240
|
-
|
256
|
+
```ruby
|
257
|
+
class User < ActiveRecord::Base
|
258
|
+
validates_with EmailAddress::ActiveRecordValidator, field: :email
|
259
|
+
end
|
260
|
+
```
|
241
261
|
|
242
262
|
#### Rails Email Address Type Attribute
|
243
263
|
|
@@ -247,9 +267,11 @@ First, you need to register the type in
|
|
247
267
|
`config/initializers/email_address.rb` along with any global
|
248
268
|
configurations you want.
|
249
269
|
|
250
|
-
|
251
|
-
|
252
|
-
|
270
|
+
```ruby
|
271
|
+
ActiveRecord::Type.register(:email_address, EmailAddress::EmailAddressType)
|
272
|
+
ActiveRecord::Type.register(:canonical_email_address,
|
273
|
+
EmailAddress::CanonicalEmailAddressType)
|
274
|
+
```
|
253
275
|
|
254
276
|
Assume the Users table contains the columns "email" and "canonical_email".
|
255
277
|
We want to normalize the address in "email" and store the canonical/unique
|
@@ -258,39 +280,42 @@ the email attribute is assigned. With the canonical_email column,
|
|
258
280
|
we can look up the User, even it the given email address didn't exactly
|
259
281
|
match the registered version.
|
260
282
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
283
|
+
```ruby
|
284
|
+
class User < ApplicationRecord
|
285
|
+
attribute :email, :email_address
|
286
|
+
attribute :canonical_email, :canonical_email_address
|
287
|
+
|
288
|
+
validates_with EmailAddress::ActiveRecordValidator,
|
289
|
+
fields: %i(email canonical_email)
|
290
|
+
|
291
|
+
def email=(email_address)
|
292
|
+
self[:canonical_email] = email_address
|
293
|
+
self[:email] = email_address
|
294
|
+
end
|
295
|
+
|
296
|
+
def self.find_by_email(email)
|
297
|
+
user = self.find_by(email: EmailAddress.normal(email))
|
298
|
+
user ||= self.find_by(canonical_email: EmailAddress.canonical(email))
|
299
|
+
user ||= self.find_by(canonical_email: EmailAddress.redacted(email))
|
300
|
+
user
|
301
|
+
end
|
302
|
+
|
303
|
+
def redact!
|
304
|
+
self[:canonical_email] = EmailAddress.redact(self.canonical_email)
|
305
|
+
self[:email] = self[:canonical_email]
|
306
|
+
end
|
307
|
+
end
|
308
|
+
```
|
285
309
|
|
286
310
|
Here is how the User model works:
|
287
311
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
312
|
+
```ruby
|
313
|
+
user = User.create(email:"Pat.Smith+registrations@gmail.com")
|
314
|
+
user.email #=> "pat.smith+registrations@gmail.com"
|
315
|
+
user.canonical_email #=> "patsmith@gmail.com"
|
316
|
+
User.find_by_email("PAT.SMITH@GMAIL.COM")
|
317
|
+
#=> #<User email="pat.smith+registrations@gmail.com">
|
318
|
+
```
|
294
319
|
|
295
320
|
The `find_by_email` method looks up a given email address by the
|
296
321
|
normalized form (lower case), then by the canonical form, then finally
|
@@ -313,17 +338,19 @@ which syntax and network validations to perform.
|
|
313
338
|
|
314
339
|
You can compare email addresses:
|
315
340
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
341
|
+
```ruby
|
342
|
+
e1 = EmailAddress.new("Clark.Kent@Gmail.com")
|
343
|
+
e2 = EmailAddress.new("clark.kent+Superman@Gmail.com")
|
344
|
+
e3 = EmailAddress.new(e2.redact)
|
345
|
+
e1.to_s #=> "clark.kent@gmail.com"
|
346
|
+
e2.to_s #=> "clark.kent+superman@gmail.com"
|
347
|
+
e3.to_s #=> "{bea3f3560a757f8142d38d212a931237b218eb5e}@gmail.com"
|
322
348
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
349
|
+
e1 == e2 #=> false (Matches by normalized address)
|
350
|
+
e1.same_as?(e2) #=> true (Matches as canonical address)
|
351
|
+
e1.same_as?(e3) #=> true (Matches as redacted address)
|
352
|
+
e1 < e2 #=> true (Compares using normalized address)
|
353
|
+
```
|
327
354
|
|
328
355
|
#### Matching
|
329
356
|
|
@@ -340,13 +367,15 @@ Matching addresses by simple patterns:
|
|
340
367
|
|
341
368
|
Usage:
|
342
369
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
370
|
+
```ruby
|
371
|
+
e = EmailAddress.new("Clark.Kent@Gmail.com")
|
372
|
+
e.matches?("gmail.com") #=> true
|
373
|
+
e.matches?("google") #=> true
|
374
|
+
e.matches?(".org") #=> false
|
375
|
+
e.matches?("g*com") #=> true
|
376
|
+
e.matches?("gmail.") #=> true
|
377
|
+
e.matches?("*kent*@") #=> true
|
378
|
+
```
|
350
379
|
|
351
380
|
### Configuration
|
352
381
|
|
@@ -354,26 +383,34 @@ You can pass an options hash on the `.new()` and helper class methods to
|
|
354
383
|
control how the library treats that address. These can also be
|
355
384
|
configured during initialization by provider and default (see below).
|
356
385
|
|
357
|
-
|
358
|
-
|
386
|
+
```ruby
|
387
|
+
EmailAddress.new("clark.kent@gmail.com",
|
388
|
+
host_validation: :syntax, host_encoding: :unicode)
|
389
|
+
```
|
359
390
|
|
360
391
|
Globally, you can change and query configuration options:
|
361
392
|
|
362
|
-
|
363
|
-
|
393
|
+
```ruby
|
394
|
+
EmailAddress::Config.setting(:host_validation, :mx)
|
395
|
+
EmailAddress::Config.setting(:host_validation) #=> :mx
|
396
|
+
```
|
364
397
|
|
365
398
|
Or set multiple settings at once:
|
366
399
|
|
367
|
-
|
400
|
+
```ruby
|
401
|
+
EmailAddress::Config.configure(local_downcase: false, host_validation: :syntax)
|
402
|
+
```
|
368
403
|
|
369
404
|
You can add special rules by domain or provider. It takes the options
|
370
405
|
above and adds the :domain_match and :exchanger_match rules.
|
371
406
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
407
|
+
```ruby
|
408
|
+
EmailAddress.define_provider('google',
|
409
|
+
domain_match: %w(gmail.com googlemail.com),
|
410
|
+
exchanger_match: %w(google.com), # Requires host_validation==:mx
|
411
|
+
local_size: 5..64,
|
412
|
+
mailbox_canonical: ->(m) {m.gsub('.','')})
|
413
|
+
```
|
377
414
|
|
378
415
|
The library ships with the most common set of provider rules. It is not meant
|
379
416
|
to house a database of all providers, but a separate `email_address-providers`
|
@@ -391,26 +428,29 @@ DNS. If you specify an exchanger pattern, but requires a DNS MX lookup.
|
|
391
428
|
For Rails application, create an initializer file with your default
|
392
429
|
configuration options:
|
393
430
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
431
|
+
```ruby
|
432
|
+
# ./config/initializers/email_address.rb
|
433
|
+
EmailAddress::Config.setting( local_format: :relaxed )
|
434
|
+
EmailAddress::Config.provider(:github,
|
435
|
+
host_match: %w(github.com), local_format: :standard)
|
436
|
+
```
|
398
437
|
|
399
438
|
#### Override Error Messaegs
|
400
439
|
|
401
440
|
You can override the default error messages as follows:
|
402
441
|
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
442
|
+
```ruby
|
443
|
+
EmailAddress::Config.error_messages(
|
444
|
+
invalid_address: "Invalid Email Address",
|
445
|
+
invalid_mailbox: "Invalid Recipient/Mailbox",
|
446
|
+
invalid_host: "Invalid Host/Domain Name",
|
447
|
+
exceeds_size: "Address too long",
|
448
|
+
not_allowed: "Address is not allowed",
|
449
|
+
incomplete_domain: "Domain name is incomplete")
|
450
|
+
```
|
410
451
|
|
411
452
|
Full translation support would be ideal though.
|
412
453
|
|
413
|
-
|
414
454
|
### Available Configuration Settings
|
415
455
|
|
416
456
|
* sha1_secret -
|
@@ -513,7 +553,6 @@ Proper personal identity can still be provided using
|
|
513
553
|
[MIME Encoded-Words](http://en.wikipedia.org/wiki/MIME#Encoded-Word)
|
514
554
|
in Email headers.
|
515
555
|
|
516
|
-
|
517
556
|
#### Email Addresses as Sensitive Data
|
518
557
|
|
519
558
|
Like Social Security and Credit Card Numbers, email addresses are
|
data/email_address.gemspec
CHANGED
data/lib/email_address.rb
CHANGED
@@ -5,6 +5,7 @@ module EmailAddress
|
|
5
5
|
require "email_address/exchanger"
|
6
6
|
require "email_address/host"
|
7
7
|
require "email_address/local"
|
8
|
+
require "email_address/rewriter"
|
8
9
|
require "email_address/address"
|
9
10
|
require "email_address/version"
|
10
11
|
require "email_address/active_record_validator" if defined?(ActiveModel)
|
@@ -13,22 +14,29 @@ module EmailAddress
|
|
13
14
|
require "email_address/canonical_email_address_type"
|
14
15
|
end
|
15
16
|
|
16
|
-
# @!method self.valid?(options={})
|
17
|
+
# @!method self.valid?(email_address, options={})
|
17
18
|
# Proxy method to {EmailAddress::Address#valid?}
|
18
|
-
# @!method self.error
|
19
|
+
# @!method self.error(email_address)
|
19
20
|
# Proxy method to {EmailAddress::Address#error}
|
20
|
-
# @!method self.normal
|
21
|
+
# @!method self.normal(email_address)
|
21
22
|
# Proxy method to {EmailAddress::Address#normal}
|
22
|
-
# @!method self.redact(
|
23
|
+
# @!method self.redact(email_address, options={})
|
23
24
|
# Proxy method to {EmailAddress::Address#redact}
|
24
|
-
# @!method self.munge
|
25
|
+
# @!method self.munge(email_address, options={})
|
25
26
|
# Proxy method to {EmailAddress::Address#munge}
|
26
|
-
# @!method self.
|
27
|
+
# @!method self.base(email_address, options{})
|
28
|
+
# Returns the base form of the email address, the mailbox
|
29
|
+
# without optional puncuation removed, no tag, and the host name.
|
30
|
+
# @!method self.canonical(email_address, options{})
|
27
31
|
# Proxy method to {EmailAddress::Address#canonical}
|
28
|
-
# @!method self.reference
|
29
|
-
#
|
32
|
+
# @!method self.reference(email_address, form=:base, options={})
|
33
|
+
# Returns the reference form of the email address, by default
|
34
|
+
# the MD5 digest of the Base Form the the address.
|
35
|
+
# @!method self.srs(email_address, sending_domain, options={})
|
36
|
+
# Returns the address encoded for SRS forwarding. Pass a local
|
37
|
+
# secret to use in options[:secret]
|
30
38
|
class << self
|
31
|
-
(%i[valid? error normal redact munge canonical reference] &
|
39
|
+
(%i[valid? error normal redact munge canonical reference base srs] &
|
32
40
|
EmailAddress::Address.public_instance_methods
|
33
41
|
).each do |proxy_method|
|
34
42
|
define_method(proxy_method) do |*args, &block|
|
@@ -6,6 +6,8 @@ module EmailAddress
|
|
6
6
|
# (EmailAddress::Local) and Host (Email::AddressHost) parts.
|
7
7
|
class Address
|
8
8
|
include Comparable
|
9
|
+
include EmailAddress::Rewriter
|
10
|
+
|
9
11
|
attr_accessor :original, :local, :host, :config, :reason
|
10
12
|
|
11
13
|
CONVENTIONAL_REGEX = /\A#{::EmailAddress::Local::CONVENTIONAL_MAILBOX_WITHIN}
|
@@ -19,19 +21,26 @@ module EmailAddress
|
|
19
21
|
# instance, and initializes the address to the "normalized" format of the
|
20
22
|
# address. The original string is available in the #original method.
|
21
23
|
def initialize(email_address, config={})
|
22
|
-
|
24
|
+
@config = config # This needs refactoring!
|
25
|
+
email_address = (email_address || "").strip
|
23
26
|
@original = email_address
|
24
|
-
email_address
|
25
|
-
|
26
|
-
|
27
|
-
else
|
28
|
-
(local, host) = [email_address, '']
|
29
|
-
end
|
27
|
+
email_address = parse_rewritten(email_address) unless config[:skip_rewrite]
|
28
|
+
local, host = EmailAddress::Address.split_local_host(email_address)
|
29
|
+
|
30
30
|
@host = EmailAddress::Host.new(host, config)
|
31
31
|
@config = @host.config
|
32
32
|
@local = EmailAddress::Local.new(local, @config, @host)
|
33
33
|
end
|
34
34
|
|
35
|
+
# Given an email address, this returns an array of [local, host] parts
|
36
|
+
def self.split_local_host(email)
|
37
|
+
if lh = email.match(/(.+)@(.+)/)
|
38
|
+
lh.to_a[1,2]
|
39
|
+
else
|
40
|
+
[email, '']
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
35
44
|
############################################################################
|
36
45
|
# Local Part (left of @) access
|
37
46
|
# * local: Access full local part instance
|
@@ -121,6 +130,11 @@ module EmailAddress
|
|
121
130
|
self.canonical == self.to_s
|
122
131
|
end
|
123
132
|
|
133
|
+
# The base address is the mailbox, without tags, and host.
|
134
|
+
def base
|
135
|
+
self.mailbox + "@" + self.hostname
|
136
|
+
end
|
137
|
+
|
124
138
|
# Returns the redacted form of the address
|
125
139
|
# This format is defined by this libaray, and may change as usage increases.
|
126
140
|
# Takes either :sha1 (default) or :md5 as the argument
|
@@ -147,15 +161,15 @@ module EmailAddress
|
|
147
161
|
# use the email address MD5 instead of the actual address to refer to the
|
148
162
|
# same shared user identity without exposing the actual address when it
|
149
163
|
# is not known in common.
|
150
|
-
def reference
|
151
|
-
Digest::MD5.hexdigest(self.
|
164
|
+
def reference(form=:base)
|
165
|
+
Digest::MD5.hexdigest(self.send(form))
|
152
166
|
end
|
153
167
|
alias :md5 :reference
|
154
168
|
|
155
169
|
# This returns the SHA1 digest (in a hex string) of the canonical email
|
156
170
|
# address. See #md5 for more background.
|
157
|
-
def sha1
|
158
|
-
Digest::SHA1.hexdigest((
|
171
|
+
def sha1(form=:base)
|
172
|
+
Digest::SHA1.hexdigest((self.send(form)||"") + (@config[:sha1_secret]||""))
|
159
173
|
end
|
160
174
|
|
161
175
|
#---------------------------------------------------------------------------
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module EmailAddress::Rewriter
|
4
|
+
|
5
|
+
SRS_FORMAT_REGEX = /\ASRS0=(....)=(\w\w)=(.+?)=(.+?)@(.+)\z/
|
6
|
+
|
7
|
+
def parse_rewritten(e)
|
8
|
+
@rewrite_scheme = nil
|
9
|
+
@rewrite_error = nil
|
10
|
+
e = parse_srs(e)
|
11
|
+
# e = parse_batv(e)
|
12
|
+
e
|
13
|
+
end
|
14
|
+
|
15
|
+
#---------------------------------------------------------------------------
|
16
|
+
# SRS (Sender Rewriting Scheme) allows an address to be forwarded from the
|
17
|
+
# original owner and encoded to be used with the domain name of the MTA (Mail
|
18
|
+
# Transport Agent). It encodes the original address within the local part of the
|
19
|
+
# sending email address and respects VERP. If example.com needs to forward a
|
20
|
+
# message from "sender@gmail.com", the SMTP envelope sender is used at this
|
21
|
+
# address. These methods respect DMARC and prevent spoofing email send using
|
22
|
+
# a different domain.
|
23
|
+
# Format: SRS0=HHH=TT=domain=local@sending-domain.com
|
24
|
+
#---------------------------------------------------------------------------
|
25
|
+
def srs(sending_domain, options={}, &block)
|
26
|
+
tt = srs_tt()
|
27
|
+
a = [tt, self.hostname, self.local.to_s].join("=") + "@" + sending_domain
|
28
|
+
hhh = srs_hash(a, options, &block)
|
29
|
+
|
30
|
+
["SRS0", hhh, a].join("=")
|
31
|
+
end
|
32
|
+
|
33
|
+
def srs?(email)
|
34
|
+
email.match(SRS_FORMAT_REGEX) ? true : false
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_srs(email, options={}, &block)
|
38
|
+
if email && email.match(SRS_FORMAT_REGEX)
|
39
|
+
@rewrite_scheme = :srs
|
40
|
+
hhh, tt, domain, local, sending_domain = [$1, $2, $3, $4, $5]
|
41
|
+
hhh = tt = sending_domain if false && hhh # Hide warnings for now :-)
|
42
|
+
a = [tt, domain, local].join("=") + "@" + sending_domain
|
43
|
+
unless srs_hash(a, options, &block) === hhh
|
44
|
+
@rewrite_error = "Invalid SRS Email Address: Possibly altered"
|
45
|
+
end
|
46
|
+
unless tt == srs_tt
|
47
|
+
@rewrite_error = "Invalid SRS Email Address: Too old"
|
48
|
+
end
|
49
|
+
[local, domain].join("@")
|
50
|
+
else
|
51
|
+
email
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# SRS Timeout Token
|
56
|
+
# Returns a 2-character code for the day. After a few days the code will roll.
|
57
|
+
# TT has a one-day resolution in order to make the address invalid after a few days.
|
58
|
+
# The cycle period is 3.5 years. Used to control late bounces and harvesting.
|
59
|
+
def srs_tt(t=Time.now.utc)
|
60
|
+
Base64.encode64((t.to_i / (60*60*24) % 210).to_s)[0,2]
|
61
|
+
end
|
62
|
+
|
63
|
+
def srs_hash(email, options={}, &block)
|
64
|
+
key = options[:key] || @config[:key] || email.reverse
|
65
|
+
if block_given?
|
66
|
+
block.call(email)[0,4]
|
67
|
+
else
|
68
|
+
Base64.encode64(Digest::SHA1.digest(email + key))[0,4]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
#---------------------------------------------------------------------------
|
73
|
+
# Returns a BATV form email address with "Private Signature" (prvs).
|
74
|
+
# Options: key: 0-9 key digit to use
|
75
|
+
# key_0..key_9: secret key used to sign/verify
|
76
|
+
# prvs_days: number of days before address "expires"
|
77
|
+
#
|
78
|
+
# BATV - Bounce Address Tag Validation
|
79
|
+
# PRVS - Simple Private Signature
|
80
|
+
# Ex: prvs=KDDDSSSS=user@example.com
|
81
|
+
# * K: Digit for Key rotation
|
82
|
+
# * DDD: Expiry date, since 1970, low 3 digits
|
83
|
+
# * SSSSSS: sha1( KDDD + orig-mailfrom + key)[0,6]
|
84
|
+
# See: https://tools.ietf.org/html/draft-levine-smtp-batv-01
|
85
|
+
#---------------------------------------------------------------------------
|
86
|
+
def batv_prvs(options={})
|
87
|
+
k = options[:prvs_key_id] || "0"
|
88
|
+
prvs_days = options[:prvs_days] || @config[:prvs_days] || 30
|
89
|
+
ddd = prvs_day(prvs_days)
|
90
|
+
ssssss = prvs_sign(k, ddd, self.to_s, options={})
|
91
|
+
["prvs=", k, ddd, ssssss, '=', self.to_s].join('')
|
92
|
+
end
|
93
|
+
|
94
|
+
PRVS_REGEX = /\Aprvs=(\d)(\d{3})(\w{6})=(.+)\z/
|
95
|
+
|
96
|
+
def parse_prvs(email, options={})
|
97
|
+
if email.match(PRVS_REGEX)
|
98
|
+
@rewrite_scheme = :prvs
|
99
|
+
k, ddd, ssssss, email = [$1, $2, $3, $4]
|
100
|
+
|
101
|
+
unless ssssss == prvs_sign(k, ddd, email, options)
|
102
|
+
@rewrite_error = "Invalid BATV Address: Signature unverified"
|
103
|
+
end
|
104
|
+
exp = ddd.to_i
|
105
|
+
roll = 1000 - exp # rolling 1000 day window
|
106
|
+
today = prvs_day(0)
|
107
|
+
# I'm sure this is wrong
|
108
|
+
if exp > today && exp < roll
|
109
|
+
@rewrite_error = "Invalid SRS Email Address: Address expired"
|
110
|
+
elsif exp < today && (today - exp) > 0
|
111
|
+
@rewrite_error = "Invalid SRS Email Address: Address expired"
|
112
|
+
end
|
113
|
+
[local, domain].join("@")
|
114
|
+
else
|
115
|
+
email
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def prvs_day(days)
|
120
|
+
((Time.now.to_i + (days*24*60*60)) / (24*60*60)).to_s[-3,3]
|
121
|
+
end
|
122
|
+
|
123
|
+
def prvs_sign(k, ddd, email, options={})
|
124
|
+
str = [ddd, ssssss, '=', self.to_s].join('')
|
125
|
+
key = options["key_#{k}".to_i] || @config["key_#{k}".to_i] || str.reverse
|
126
|
+
Digest::SHA1.hexdigest([k,ddd, email, key].join(''))[0,6]
|
127
|
+
end
|
128
|
+
|
129
|
+
#---------------------------------------------------------------------------
|
130
|
+
# VERP Embeds a recipient email address into the bounce address
|
131
|
+
# Bounce Address: message-id@example.net
|
132
|
+
# Recipient Email: recipient@example.org
|
133
|
+
# VERP : message-id+recipient=example.org@example.net
|
134
|
+
# To handle incoming verp, the "tag" is the recipient email address,
|
135
|
+
# remember to convert the last '=' into a '@' to reconstruct it.
|
136
|
+
#---------------------------------------------------------------------------
|
137
|
+
def verp(recipient, split_char='+')
|
138
|
+
self.local.to_s +
|
139
|
+
split_char + recipient.gsub("@","=") +
|
140
|
+
"@" + self.hostname
|
141
|
+
end
|
142
|
+
|
143
|
+
# NEXT: DMARC, SPF Validation
|
144
|
+
|
145
|
+
end
|
@@ -29,10 +29,12 @@ class TestAddress < Minitest::Test
|
|
29
29
|
def test_forms
|
30
30
|
a = EmailAddress.new("User+tag@example.com")
|
31
31
|
assert_equal "user+tag@example.com", a.to_s
|
32
|
+
assert_equal "user@example.com", a.base
|
32
33
|
assert_equal "user@example.com", a.canonical
|
33
34
|
assert_equal "{63a710569261a24b3766275b7000ce8d7b32e2f7}@example.com", a.redact
|
34
35
|
assert_equal "{b58996c504c5638798eb6b511e6f49af}@example.com", a.redact(:md5)
|
35
36
|
assert_equal "b58996c504c5638798eb6b511e6f49af", a.reference
|
37
|
+
assert_equal "6bdd00c53645790ad9bbcb50caa93880", EmailAddress.reference("Gmail.User+tag@gmail.com")
|
36
38
|
end
|
37
39
|
|
38
40
|
# COMPARISON & MATCHING
|
@@ -58,10 +60,10 @@ class TestAddress < Minitest::Test
|
|
58
60
|
|
59
61
|
def test_empty_address
|
60
62
|
a = EmailAddress.new("")
|
61
|
-
assert_equal "{
|
63
|
+
assert_equal "{9a78211436f6d425ec38f5c4e02270801f3524f8}", a.redact
|
62
64
|
assert_equal "", a.to_s
|
63
65
|
assert_equal "", a.canonical
|
64
|
-
assert_equal "
|
66
|
+
assert_equal "518ed29525738cebdac49c49e60ea9d3", a.reference
|
65
67
|
end
|
66
68
|
|
67
69
|
# VALIDATION
|
@@ -89,6 +91,14 @@ class TestAddress < Minitest::Test
|
|
89
91
|
assert "aasdf-34-.z@example.com".match(EmailAddress::Address::RELAXED_REGEX)
|
90
92
|
end
|
91
93
|
|
94
|
+
def test_srs
|
95
|
+
ea= "first.LAST+tag@gmail.com"
|
96
|
+
e = EmailAddress.new(ea)
|
97
|
+
s = e.srs("example.com")
|
98
|
+
assert s.match(EmailAddress::Address::SRS_FORMAT_REGEX)
|
99
|
+
assert EmailAddress.new(s).to_s == e.to_s
|
100
|
+
end
|
101
|
+
|
92
102
|
# Quick Regression tests for addresses that should have been valid (but fixed)
|
93
103
|
def test_issues
|
94
104
|
assert true, EmailAddress.valid?('test@jiff.com', dns_lookup: :mx) # #7
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require_relative '../test_helper'
|
3
|
+
|
4
|
+
class TestRewriter < Minitest::Test
|
5
|
+
|
6
|
+
def test_srs
|
7
|
+
ea= "first.LAST+tag@gmail.com"
|
8
|
+
e = EmailAddress.new(ea)
|
9
|
+
s = e.srs("example.com")
|
10
|
+
assert s.match(EmailAddress::Address::SRS_FORMAT_REGEX)
|
11
|
+
assert EmailAddress.new(s).to_s == e.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: email_address
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Allen Fair
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-01-
|
11
|
+
date: 2018-01-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -112,16 +112,16 @@ dependencies:
|
|
112
112
|
name: netaddr
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
|
-
- - "
|
115
|
+
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version:
|
117
|
+
version: 1.5.1
|
118
118
|
type: :runtime
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
|
-
- - "
|
122
|
+
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version:
|
124
|
+
version: 1.5.1
|
125
125
|
description: The EmailAddress Gem to work with and validate email addresses.
|
126
126
|
email:
|
127
127
|
- allen.fair@gmail.com
|
@@ -145,6 +145,7 @@ files:
|
|
145
145
|
- lib/email_address/exchanger.rb
|
146
146
|
- lib/email_address/host.rb
|
147
147
|
- lib/email_address/local.rb
|
148
|
+
- lib/email_address/rewriter.rb
|
148
149
|
- lib/email_address/version.rb
|
149
150
|
- test/activerecord/test_ar.rb
|
150
151
|
- test/activerecord/user.rb
|
@@ -153,6 +154,7 @@ files:
|
|
153
154
|
- test/email_address/test_exchanger.rb
|
154
155
|
- test/email_address/test_host.rb
|
155
156
|
- test/email_address/test_local.rb
|
157
|
+
- test/email_address/test_rewriter.rb
|
156
158
|
- test/test_email_address.rb
|
157
159
|
- test/test_helper.rb
|
158
160
|
homepage: https://github.com/afair/email_address
|
@@ -175,7 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
175
177
|
version: '0'
|
176
178
|
requirements: []
|
177
179
|
rubyforge_project:
|
178
|
-
rubygems_version: 2.
|
180
|
+
rubygems_version: 2.7.3
|
179
181
|
signing_key:
|
180
182
|
specification_version: 4
|
181
183
|
summary: This gem provides a ruby language library for working with and validating
|
@@ -190,5 +192,6 @@ test_files:
|
|
190
192
|
- test/email_address/test_exchanger.rb
|
191
193
|
- test/email_address/test_host.rb
|
192
194
|
- test/email_address/test_local.rb
|
195
|
+
- test/email_address/test_rewriter.rb
|
193
196
|
- test/test_email_address.rb
|
194
197
|
- test/test_helper.rb
|