email_address 0.1.7 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|