email_address 0.0.1 → 0.0.2

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/README.md +99 -32
  4. data/email_address.gemspec +2 -1
  5. data/lib/email_address.rb +14 -142
  6. data/lib/email_address/address.rb +70 -0
  7. data/lib/email_address/config.rb +75 -0
  8. data/lib/email_address/domain_matcher.rb +90 -0
  9. data/lib/email_address/domain_parser.rb +71 -0
  10. data/lib/email_address/exchanger.rb +67 -0
  11. data/lib/email_address/host.rb +71 -1
  12. data/lib/email_address/local.rb +97 -0
  13. data/lib/email_address/validator.rb +135 -0
  14. data/lib/email_address/version.rb +1 -1
  15. data/test/email_address/test_address.rb +30 -0
  16. data/test/email_address/test_config.rb +13 -0
  17. data/test/email_address/test_domain_matcher.rb +15 -0
  18. data/test/email_address/test_domain_parser.rb +29 -0
  19. data/test/email_address/test_exchanger.rb +19 -0
  20. data/test/email_address/test_host.rb +43 -0
  21. data/test/email_address/test_local.rb +27 -0
  22. data/test/email_address/test_validator.rb +16 -0
  23. data/test/test_email_address.rb +12 -0
  24. metadata +40 -27
  25. data/lib/email_address/esp.rb +0 -4
  26. data/lib/email_address/providers/default.rb +0 -8
  27. data/lib/email_address/providers/google.rb +0 -8
  28. data/lib/email_providers/address.rb +0 -102
  29. data/lib/email_providers/config.rb +0 -36
  30. data/lib/email_providers/factory.rb +0 -17
  31. data/lib/email_providers/host.rb +0 -87
  32. data/lib/email_providers/mail_exchanger.rb +0 -60
  33. data/lib/email_providers/mailbox.rb +0 -44
  34. data/lib/email_providers/providers/default.rb +0 -55
  35. data/lib/email_providers/providers/google.rb +0 -27
  36. data/lib/email_providers/version.rb +0 -3
  37. data/test/email_address.rb +0 -52
  38. data/test/email_address/address.rb +0 -16
  39. data/test/email_address/config.rb +0 -13
  40. data/test/email_address/host.rb +0 -29
  41. data/test/email_address/mail_exchanger.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 225554c1b1abf4ead2900a7209b55ec7babeaefe
4
- data.tar.gz: 4de853f82e80be5e9e6ce843a16fd2dc3f4a3255
3
+ metadata.gz: da957eceefc70a7dc4a21a1d1f44296e36783021
4
+ data.tar.gz: fffc1230d4c3080ab56229e34604838543b659e6
5
5
  SHA512:
6
- metadata.gz: 11b73a88096b1553dac4aa7979a1a7385c568ae8d5c2dca268c88bf1fd2827aa77bb76e3a412a6cc85d1f5d9983f470a1a6b1840df73d428fbfdae76277bfe9a
7
- data.tar.gz: 97b3f3497d00346404b94dbfde915bbbcf1dae41bcbc6d7fc98a4e0dc0072245b57597ca4939295e6db47fc2700ed935add5f8019519389950288f2501ad7bc6
6
+ metadata.gz: e638d9efbba9834cd1857150db91821d82e6bbbbf95ee694bc30287d5c9f69893d87928ee49b044207e9503accf4eefc354295b29e3d5ec44ad6ee5ab2918653
7
+ data.tar.gz: 90749c6bdbc84c2586208eefb04f8eb208812124d3253170d46471b64df4e23c5ff24bc6967c426285e8cd838f71bcd1fb3b910a7a82690ab3a91da5665e9e40
data/Gemfile CHANGED
@@ -2,3 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in email_address.gemspec
4
4
  gemspec
5
+ gem 'minitest'
data/README.md CHANGED
@@ -1,25 +1,31 @@
1
- # EmailAddress
2
-
3
- The EmailAddress library is an _opinionated_ email address handler and
4
- validator.
5
-
6
- So you have an email address input by a user. Do you want to validate
7
- it, check it for uniqueness, or mine statistics on all your addresses?
8
- Then the email_address gem is for you!
9
-
10
- Opininated? Yes. By default, this does not support RFC822 specification
11
- because it allows addresses that should never exist for real people.
12
- By limiting email addresses to a subset of standardly used ones, you can
13
- remove false positives in validation, and help the email community
14
- evolve into a friendlier place. I like standards as much as the next
15
- person, but enough is enough!
16
-
17
- Why my opinion? I've been working with email and such since the late
18
- 1990's. I would like to see modern practices applied to email addresses.
19
- These rules really do apply to most (without real statistics, I'll claim
20
- 99.9+%) usage in the 21st Century. They may not be for everyone, you may
21
- need strict adherence to these standards for historical reasons, but I
22
- bet you'll wish you could support this one instead!
1
+ # Email Address
2
+
3
+ The EmailAddress gem is an _opinionated_ email address handler and
4
+ validator. It does not use RFC standards because they make things worse.
5
+ Email addresses should conform to a few practices that are not
6
+ RFC-Compliant.
7
+
8
+ Specifically, local parts (left side of the @):
9
+
10
+ * Should not be case sensitive.
11
+ * Should not contain spaces or anything that would cause quoting.
12
+ * Should not allow Unicode. Addressable items like this need to be
13
+ entered from any keyboard, such as the US ASCII character set. (Domain
14
+ names too, but that can be handled with Punycode.)
15
+ * Should not have comments. Neither should domains.
16
+ * Should not allow unusual symbols (not usually in names and standard
17
+ punctuation).
18
+ * Should not be verified by SMTP connections if possible.
19
+ * Should have spaces stripped automatically if enabled
20
+ * Should be of a reasonable length to identify the recipient.
21
+ * Should be human readable and writable.
22
+ * Should continue allowing for tagging.
23
+ * Should provide mechanism for handling bounce backs and VERP.
24
+ * Should be easily normalized and corrected.
25
+ * Should be canonicalized to identify duplicates if necessary.
26
+ * Should be able to be stored as a digest for privacy proctections.
27
+
28
+ If you're on board, let's go!
23
29
 
24
30
  ## Installation
25
31
 
@@ -40,23 +46,84 @@ Or install it yourself as:
40
46
  Inspect your email address string by creating an instance of
41
47
  EmailAddress:
42
48
 
43
- email = EmailAddress.new("user@example.com")
49
+ email = EmailAddress.new("USER+tag@EXAMPLE.com")
50
+ email.normalize #=> "user+tag@example.com"
51
+ email.canonical #=> "user@example.com"
52
+
53
+ Email Service Provider (ESP) specific edits can be created to provide
54
+ validations and canonical manipulations. A few are given out of the box.
55
+ Providers can be defined bu email domain match rules, or by match rules
56
+ for the MX host names using domains or CIDR addresses.
57
+
58
+ email = EmailAddress.new("First.Last+Tag@Gmail.Com")
59
+ email.provider #=> :gmail
60
+ email.canonical #=> "firstlast@gmail.com"
61
+
62
+ Storing the canonical address with the request address (don't remove
63
+ tags given by users), you can lookup email addresses without the
64
+ original formatting, case, and tag information.
65
+
66
+ You can inspect the MX (Mail Exchanger) records
67
+
68
+ email.host.exchanger.mxers.first
69
+ #=> {:host=>"alt3.gmail-smtp-in.l.google.com", :ip=>"173.194.70.27", :priority=>30}
44
70
 
45
71
  You can see if it validates as an opinionated address:
46
72
 
47
- email.valid?
73
+ email.valid? # Resonably valid?
74
+ email.valid_host? # Host name is defined in DNS
75
+ email.strict? # Strictly valid?
76
+
77
+ Email addresses should be able to be "archived" or stored in a digest
78
+ format. This allows you to safely keep a record of the address and still
79
+ protect the account's privacy after it has been closed. Given an address
80
+ for inquiry, it can still look up a closed account.
81
+
82
+ email.md5 #=> "dea073fb289e438a6d69c5384113454c"
83
+ email.archive #=> "554d32017ab3a7fcf51c88ffce078689003bc521@gmail.com"
84
+
85
+
86
+ ## Email Address Parts
87
+
88
+ The Local and Domain Parsing routines divvy the email address into these
89
+ parts from `(comment)mailbox+tag@subdomain.domain.tld`
90
+
91
+ * Local - Everything to the left of the "@"
92
+ * Mailbox - The controlling mailbox name
93
+ * Tag - Anything after the mailbox and tag separator character (usually "+")
94
+ * Comment - To be removed from the normalized form
95
+ * Host Name - Everything to the right of the "@"
96
+ * Subdomains - of the host name, if any.
97
+ * Domain Name - host name without subdomains, with TLD
98
+ * TLD - the rightmost word or set of 2-character domains ("co.uk")
99
+ * Registration Name - host name without subdomain or TLD
100
+
101
+ ## Domain Matching
102
+
103
+ You can also employ domain matching rules
104
+
105
+ email.host.matches?('gmail.com', '.us', '.msn.com', 'yahoo')
106
+
107
+ This tests the address can be matched in the given list of domain rules:
108
+
109
+ * Full host name. (subdomain.example.com)
110
+ * TLD and domain wildcards (.us, .msg.com)
111
+ * Registration names matching without the TLD. 'yahoo' matches:
112
+ * "www.yahoo.com" (with Subdomains)
113
+ * "yahoo.ca" (any TLD)
114
+ * "yahoo.co.jp" (2-char TLD with 2-char Second-level)
115
+ * But _may_ also match non-Yahoo domain names (yahoo.xxx)
48
116
 
49
- This runs the following checks you can do yourself:
117
+ ## Customizing
50
118
 
51
- email.valid_format?
52
- email.valid_domain?
53
- email.valid_user?
54
- email.disposable_email?
55
- email.spam_trap?
119
+ You can change configuration options and add new providers such as:
56
120
 
57
- Of course, the last couple tests can't be published, so you can provide
58
- a callback to check them yourself if you need.
121
+ EmailAddress::Config.setup do
122
+ provider :github, domains:%w(github.com github.io)
123
+ option :check_dns, false
124
+ end
59
125
 
126
+ See `lib/email_address/config.rb` for more options.
60
127
 
61
128
  ## Contributing
62
129
 
@@ -21,5 +21,6 @@ validator.}
21
21
 
22
22
  spec.add_development_dependency "bundler", "~> 1.3"
23
23
  spec.add_development_dependency "rake"
24
- spec.add_development_dependency "simpleidn"
24
+ spec.add_dependency "simpleidn"
25
+ spec.add_dependency "netaddr"
25
26
  end
data/lib/email_address.rb CHANGED
@@ -1,145 +1,17 @@
1
- #require "email_address/version"
2
- #require "email_address/config"
3
- #require "email_address/host"
4
- #require "email_address/address"
5
- require 'simpleidn'
6
-
7
- class EmailAddress
8
- attr_reader :address, :local, :account, :tag, :comment,
9
- :domain, :subdomains, :domain_name, :base_domain, :top_level_domain
10
-
11
- def initialize(email)
12
- self.address = email
13
- end
14
-
15
- ##############################################################################
16
- # Basic email address: local@domain
17
- # Only supporting FQDN's (Fully Qualified Domain Names)?
18
- # Length: Up to 254 characters
19
- ##############################################################################
20
- def address=(email)
21
- @address = email.strip
22
- (local_name, domain_name) = @address.split('@')
23
- self.domain = domain_name
24
- self.local = local_name
25
- @address
26
- end
27
-
28
- def to_s
29
- [local, domain].join('@')
30
- end
31
-
32
- ##############################################################################
33
- # Domain Parsing
34
- # Parts: subdomains.basedomain.top-level-domain
35
- # IPv6/IPv6: [128.0.0.1], [IPv6:2001:db8:1ff::a0b:dbd0]
36
- # Comments: (comment)example.com, example.com(comment)
37
- # Internationalized: Unicode to Punycode
38
- # Length: up to 255 characters
39
- ##############################################################################
40
- def domain=(host_name)
41
- host_name ||= ''
42
- @domain = host_name.strip.downcase
43
- parse_domain
44
- @domain
45
- end
46
-
47
- def parse_domain
48
- @subdomains = @base_domain = @domain_name = @top_level_domain = ''
49
- # Patterns: *.com, *.xx.cc, *.cc
50
- if @domain =~ /\A(.+)\.(\w{3,10})\z/ || @domain =~ /\A(.+)\.(\w{1,3}\.\w\w)\z/ || @domain =~ /\A(.+)\.(\w\w)\z/
51
- @top_level_domain = $2;
52
- sld = $1 # Second level domain
53
- if sld =~ /\A(.+)\.(.+)\z/ # is subdomain? sub.example [.tld]
54
- @subdomains = $1
55
- @base_domain = $2
56
- else
57
- @subdomains = ""
58
- @base_domain = sld
59
- end
60
- @domain_name = @base_domain + '.' + @top_level_domain
61
- end
62
- end
63
-
64
- def dns_hostname
65
- @dns_hostname ||= SimpleIDN.to_ascii(domain)
66
- end
67
-
68
- ##############################################################################
69
- # Parsing id provider-dependent, but RFC allows:
70
- # A-Z a-z 0-9 . ! # $ % ' * + - / = ? ^ _ { | } ~
71
- # Quoted: space ( ) , : ; < > @ [ ]
72
- # Quoted-Backslash-Escaped: \ "
73
- # Quote local part or dot-separated sub-parts x."y".z
74
- # (comment)mailbox | mailbox(comment)
75
- # 8-bit/UTF-8: allowed but mail-system defined
76
- # RFC 5321 also warns that "a host that expects to receive mail SHOULD avoid defining mailboxes where the Local-part requires (or uses) the Quoted-string form".
77
- # Postmaster: must always be case-insensitive
78
- # Case: sensitive, but usually treated as equivalent
79
- # Local Parts: comment, account tag
80
- # Length: upt o 64 cgaracters
81
- ##############################################################################
82
- def local=(local)
83
- local ||= ''
84
- @local = local.strip.downcase
85
- @account = parse_comment(@local)
86
- (@account, @tag) = @account.split(tag_separator)
87
- @tag ||= ''
88
-
89
- @local
90
- end
91
-
92
- def parse_comment(local)
93
- if local =~ /\A\((.+?)\)(.+)\z/
94
- (@comment, local) = [$1, $2]
95
- elsif @local =~ /\A(.+)\((.+?)\)\z/
96
- (@comment, local) = [$1, $2]
97
- else
98
- @comment = '';
99
- end
100
- local
101
- end
102
-
103
- ##############################################################################
104
- # Provider-Specific Settings
105
- ##############################################################################
106
-
107
- def provider
108
- # @provider ||= EmailProviders::Default.new
109
- 'unknown'
110
- end
111
-
112
- def tag_separator
113
- '+'
114
- end
115
-
116
- def case_sensitive_local
117
- false
118
- end
119
-
120
- # Returns the unique address as simplified account@hostname
121
- def unique_address
122
- "#{account}@#{dns_hostname}".downcase
123
- end
124
-
125
- # Letters, numbers, period (no start) 6-30chars
126
- def user_pattern
127
- /\A[a-z0-9][\.a-z0-9]{0,29}\z/i
128
- end
129
-
130
- ##############################################################################
131
- # Validations -- Eventually a provider-sepecific check
132
- ##############################################################################
133
- def valid?
134
- return false unless @local =~ user_pattern
135
- return false unless provider # .valid_domain
136
- true
137
- end
138
-
139
- def valid_format?
140
- return false unless @local.match(user_pattern)
141
- return false unless @host.valid_format?
142
- true
1
+ require "email_address/address"
2
+ require "email_address/config"
3
+ require "email_address/domain_matcher"
4
+ require "email_address/domain_parser"
5
+ require "email_address/exchanger"
6
+ require "email_address/host"
7
+ require "email_address/local"
8
+ require "email_address/validator"
9
+ require "email_address/version"
10
+
11
+ module EmailAddress
12
+
13
+ def self.new(address)
14
+ EmailAddress::Address.new(address)
143
15
  end
144
16
 
145
17
  end
@@ -1,4 +1,74 @@
1
+ require 'digest/sha1'
2
+ require 'digest/md5'
3
+
1
4
  module EmailAddress
2
5
  class Address
6
+
7
+ def initialize(address)
8
+ @address = address
9
+ parse
10
+ end
11
+
12
+ def parse
13
+ (_, local, host) = @address.match(/\A(.+)@(.+)/).to_a
14
+ @host = EmailAddress::Host.new(host)
15
+ @local = EmailAddress::Local.new(local, @host.provider)
16
+ end
17
+
18
+ def host
19
+ @host
20
+ end
21
+
22
+ def local
23
+ @local
24
+ end
25
+
26
+ def mailbox
27
+ @local.mailbox
28
+ end
29
+
30
+ def host_name
31
+ @host.host_name
32
+ end
33
+
34
+ def tag
35
+ @local.tag
36
+ end
37
+
38
+ def comment
39
+ @local.comment
40
+ end
41
+
42
+ def provider
43
+ @host.provider
44
+ end
45
+
46
+ def to_s
47
+ normalize
48
+ end
49
+
50
+ def normalize
51
+ [@local.normalize, @host.normalize].join('@')
52
+ end
53
+
54
+ def canonical
55
+ [@local.canonical, @host.canonical].join('@')
56
+ end
57
+
58
+ def md5
59
+ Digest::MD5.hexdigest(canonical)
60
+ end
61
+
62
+ def sha1
63
+ Digest::SHA1.hexdigest(canonical)
64
+ end
65
+
66
+ def archive
67
+ [sha1, @host.canonical].join('@')
68
+ end
69
+
70
+ def valid?(options={})
71
+ EmailAddress::Validator.validate(self, options)
72
+ end
3
73
  end
4
74
  end
@@ -1,4 +1,79 @@
1
1
  module EmailAddress
2
2
  class Config
3
+ @options = {
4
+ downcase_mailboxes: true,
5
+ check_dns: true,
6
+ }
7
+
8
+ @providers = {
9
+ default: {
10
+ domains: [],
11
+ exchangers: [],
12
+ tag_separator: '+',
13
+ case_sensitive: false,
14
+ address_size: 3..254,
15
+ local_size: 1..64,
16
+ domain_size: 1..253,
17
+ mailbox_size: 1..64,
18
+ mailbox_unicode: false,
19
+ canonical_mailbox: ->(m) {m},
20
+ valid_mailbox: nil, # :legible, :rfc, ->(m) {true}
21
+ },
22
+ aol: {
23
+ registration_names: %w(aol compuserve netscape aim cs)
24
+ },
25
+ google: {
26
+ domains: %w(gmail.com googlemail.com),
27
+ exchangers: %w(google.com),
28
+ local_size: 5..64,
29
+ canonical_mailbox: ->(m) {m.gsub('.','')},
30
+ #valid_mailbox: ->(local) { local.mailbox =~ /\A[a-z0-9][\.a-z0-9]{5,29}\z/i},
31
+ },
32
+ msn: {
33
+ valid_mailbox: ->(m) { m =~ /\A[a-z0-9][\.\-a-z0-9]{5,29}\z/i},
34
+ },
35
+ yahoo: {
36
+ domains: %w(yahoo ymail rocketmail),
37
+ exchangers: %w(yahoodns yahoo-inc),
38
+ },
39
+ }
40
+
41
+ def self.providers
42
+ @providers
43
+ end
44
+
45
+ def self.options
46
+ @options
47
+ end
48
+
49
+ def self.provider(name)
50
+ @providers[:default].merge(@providers.fetch(name) { Hash.new })
51
+ end
52
+
53
+ class Setup
54
+ attr_reader :providers
55
+
56
+ def initialize
57
+ @providers = {}
58
+ end
59
+
60
+ def do_block(&block)
61
+ instance_eval(&block)
62
+ end
63
+
64
+ def provider(name, defn={})
65
+ EmailAddress::Config.providers[name] = defn
66
+ end
67
+
68
+ def option(name, value)
69
+ EmailAddress::Config.options[name.to_sym] = value
70
+ end
71
+ end
72
+
73
+ def self.setup(&block)
74
+ @setup ||= Setup.new
75
+ @setup.do_block(&block) if block_given?
76
+ @setup
77
+ end
3
78
  end
4
79
  end