philiprehberger-email_validator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE +21 -0
- data/README.md +112 -0
- data/lib/philiprehberger/email_validator/disposable.rb +107 -0
- data/lib/philiprehberger/email_validator/mx_check.rb +82 -0
- data/lib/philiprehberger/email_validator/result.rb +48 -0
- data/lib/philiprehberger/email_validator/syntax.rb +142 -0
- data/lib/philiprehberger/email_validator/version.rb +7 -0
- data/lib/philiprehberger/email_validator.rb +114 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 66d9d94a5c449968eb64ab42f3c2a852b71ad9730f8135d6a7d43a39382c5fdd
|
|
4
|
+
data.tar.gz: 4e949ee122c5a9b8feb40b0ad4c76f069c02d661b3b1dd0603bc1e2b1c7d85c6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a54fc05245f37e0f56e0f1477e856b2bf4beeb63e758f57e5e6b17f3b107df9be24911eca192002e9a42d300b8dff5825b0343e54acb3e170e4cd6b9209031b1
|
|
7
|
+
data.tar.gz: 67b174b8a5b4fe73ce3ef81498ebc8335b80f104b28bd99aed6c27884b277049a097d75220f45385a98a649c7324829f372692e77f6842b0a52ebb04f4224a00
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this gem will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- RFC 5322 email syntax validation with local part and domain rules
|
|
15
|
+
- MX record verification using Ruby stdlib Resolv
|
|
16
|
+
- Disposable email domain detection with built-in list of ~50 providers
|
|
17
|
+
- Role-based address detection (admin@, info@, support@, etc.)
|
|
18
|
+
- Result value object with errors, warnings, and valid? predicate
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 philiprehberger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# philiprehberger-email_validator
|
|
2
|
+
|
|
3
|
+
[](https://github.com/philiprehberger/rb-email-validator/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/philiprehberger-email_validator)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/sponsors/philiprehberger)
|
|
7
|
+
|
|
8
|
+
RFC-compliant email validation with MX record verification
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Ruby >= 3.1
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add to your Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "philiprehberger-email_validator"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install directly:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gem install philiprehberger-email_validator
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require "philiprehberger/email_validator"
|
|
32
|
+
|
|
33
|
+
Philiprehberger::EmailValidator.valid?("user@example.com")
|
|
34
|
+
# => true
|
|
35
|
+
|
|
36
|
+
Philiprehberger::EmailValidator.valid?("not-an-email")
|
|
37
|
+
# => false
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Full Validation
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
result = Philiprehberger::EmailValidator.validate("user@example.com")
|
|
44
|
+
result.valid? # => true
|
|
45
|
+
result.errors # => []
|
|
46
|
+
result.warnings # => []
|
|
47
|
+
|
|
48
|
+
result = Philiprehberger::EmailValidator.validate("admin@example.com")
|
|
49
|
+
result.valid? # => true
|
|
50
|
+
result.warnings # => ["address appears to be role-based"]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### MX Record Verification
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
result = Philiprehberger::EmailValidator.validate("user@example.com", check_mx: true)
|
|
57
|
+
result.valid? # => true (if domain has MX/A records)
|
|
58
|
+
|
|
59
|
+
Philiprehberger::EmailValidator.mx_valid?("example.com")
|
|
60
|
+
# => true
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Disposable Domain Detection
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
Philiprehberger::EmailValidator.disposable?("user@mailinator.com")
|
|
67
|
+
# => true
|
|
68
|
+
|
|
69
|
+
result = Philiprehberger::EmailValidator.validate("user@mailinator.com", allow_disposable: false)
|
|
70
|
+
result.valid? # => false
|
|
71
|
+
result.errors # => ["disposable email domains are not allowed"]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Role-Based Address Detection
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
Philiprehberger::EmailValidator.role_based?("info@example.com")
|
|
78
|
+
# => true
|
|
79
|
+
|
|
80
|
+
Philiprehberger::EmailValidator.role_based?("alice@example.com")
|
|
81
|
+
# => false
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
| Method | Description |
|
|
87
|
+
|--------|-------------|
|
|
88
|
+
| `EmailValidator.valid?(email)` | Quick syntax check, returns boolean |
|
|
89
|
+
| `EmailValidator.validate(email, check_mx: false, allow_disposable: true)` | Full validation returning Result |
|
|
90
|
+
| `EmailValidator.mx_valid?(domain)` | Check if domain has MX or A records |
|
|
91
|
+
| `EmailValidator.disposable?(email)` | Check if email uses a disposable domain |
|
|
92
|
+
| `EmailValidator.role_based?(email)` | Detect role-based addresses (info@, admin@, etc.) |
|
|
93
|
+
|
|
94
|
+
### `Result`
|
|
95
|
+
|
|
96
|
+
| Method | Description |
|
|
97
|
+
|--------|-------------|
|
|
98
|
+
| `#valid?` | True if no validation errors |
|
|
99
|
+
| `#errors` | Array of error message strings |
|
|
100
|
+
| `#warnings` | Array of warning message strings |
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
bundle install
|
|
106
|
+
bundle exec rspec
|
|
107
|
+
bundle exec rubocop
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module EmailValidator
|
|
5
|
+
# Disposable (throwaway) email domain detection.
|
|
6
|
+
#
|
|
7
|
+
# Maintains a built-in list of commonly used disposable email providers.
|
|
8
|
+
# Useful for preventing sign-ups with temporary email addresses.
|
|
9
|
+
module Disposable
|
|
10
|
+
# Built-in list of common disposable email domains.
|
|
11
|
+
DOMAINS = Set.new(%w[
|
|
12
|
+
mailinator.com
|
|
13
|
+
guerrillamail.com
|
|
14
|
+
guerrillamail.de
|
|
15
|
+
guerrillamail.net
|
|
16
|
+
guerrillamail.org
|
|
17
|
+
tempmail.com
|
|
18
|
+
temp-mail.org
|
|
19
|
+
throwaway.email
|
|
20
|
+
sharklasers.com
|
|
21
|
+
guerrillamailblock.com
|
|
22
|
+
grr.la
|
|
23
|
+
dispostable.com
|
|
24
|
+
yopmail.com
|
|
25
|
+
yopmail.fr
|
|
26
|
+
trashmail.com
|
|
27
|
+
trashmail.me
|
|
28
|
+
trashmail.net
|
|
29
|
+
mailnesia.com
|
|
30
|
+
maildrop.cc
|
|
31
|
+
discard.email
|
|
32
|
+
mailcatch.com
|
|
33
|
+
fakeinbox.com
|
|
34
|
+
mailnull.com
|
|
35
|
+
tempail.com
|
|
36
|
+
tempr.email
|
|
37
|
+
einrot.com
|
|
38
|
+
getnada.com
|
|
39
|
+
jetable.org
|
|
40
|
+
mohmal.com
|
|
41
|
+
burpcollaborator.net
|
|
42
|
+
mailsac.com
|
|
43
|
+
harakirimail.com
|
|
44
|
+
tmail.ws
|
|
45
|
+
guerrillamail.info
|
|
46
|
+
mytemp.email
|
|
47
|
+
tempmailaddress.com
|
|
48
|
+
mailforspam.com
|
|
49
|
+
safetymail.info
|
|
50
|
+
trashymail.com
|
|
51
|
+
mailexpire.com
|
|
52
|
+
tempinbox.com
|
|
53
|
+
spamgourmet.com
|
|
54
|
+
mintemail.com
|
|
55
|
+
mailzilla.com
|
|
56
|
+
anonbox.net
|
|
57
|
+
binkmail.com
|
|
58
|
+
bobmail.info
|
|
59
|
+
chammy.info
|
|
60
|
+
deadaddress.com
|
|
61
|
+
despammed.com
|
|
62
|
+
devnullmail.com
|
|
63
|
+
dontreg.com
|
|
64
|
+
e4ward.com
|
|
65
|
+
emailigo.de
|
|
66
|
+
]).freeze
|
|
67
|
+
|
|
68
|
+
class << self
|
|
69
|
+
# Check if an email address uses a known disposable domain.
|
|
70
|
+
#
|
|
71
|
+
# @param email [String] the email address to check
|
|
72
|
+
# @return [Boolean] true if the domain is in the disposable list
|
|
73
|
+
def disposable?(email)
|
|
74
|
+
return false unless email.is_a?(String)
|
|
75
|
+
|
|
76
|
+
domain = extract_domain(email)
|
|
77
|
+
return false if domain.nil?
|
|
78
|
+
|
|
79
|
+
DOMAINS.include?(domain.downcase)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if a domain is in the disposable list.
|
|
83
|
+
#
|
|
84
|
+
# @param domain [String] the domain to check
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def domain_disposable?(domain)
|
|
87
|
+
return false unless domain.is_a?(String)
|
|
88
|
+
|
|
89
|
+
DOMAINS.include?(domain.strip.downcase)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Extract the domain from an email address.
|
|
95
|
+
#
|
|
96
|
+
# @param email [String]
|
|
97
|
+
# @return [String, nil]
|
|
98
|
+
def extract_domain(email)
|
|
99
|
+
parts = email.strip.split('@', 2)
|
|
100
|
+
return nil if parts.length != 2
|
|
101
|
+
|
|
102
|
+
parts[1]&.downcase
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'resolv'
|
|
4
|
+
|
|
5
|
+
module Philiprehberger
|
|
6
|
+
module EmailValidator
|
|
7
|
+
# MX record verification using Ruby's built-in Resolv library.
|
|
8
|
+
#
|
|
9
|
+
# Checks whether a domain has valid MX records, falling back to
|
|
10
|
+
# A record lookup as permitted by RFC 5321 section 5.1.
|
|
11
|
+
module MxCheck
|
|
12
|
+
# Default DNS timeout in seconds.
|
|
13
|
+
DEFAULT_TIMEOUT = 5
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Check if a domain has valid MX records.
|
|
17
|
+
#
|
|
18
|
+
# Falls back to checking A records if no MX records are found,
|
|
19
|
+
# as RFC 5321 permits mail delivery to the A record host.
|
|
20
|
+
#
|
|
21
|
+
# @param domain [String] the domain to check
|
|
22
|
+
# @param timeout [Integer] DNS query timeout in seconds
|
|
23
|
+
# @return [Boolean] true if the domain has MX or A records
|
|
24
|
+
def valid?(domain, timeout: DEFAULT_TIMEOUT)
|
|
25
|
+
return false if domain.nil? || domain.strip.empty?
|
|
26
|
+
|
|
27
|
+
resolver = Resolv::DNS.new
|
|
28
|
+
resolver.timeouts = timeout
|
|
29
|
+
|
|
30
|
+
mx_records = fetch_mx_records(resolver, domain)
|
|
31
|
+
return true unless mx_records.empty?
|
|
32
|
+
|
|
33
|
+
a_records = fetch_a_records(resolver, domain)
|
|
34
|
+
!a_records.empty?
|
|
35
|
+
rescue Resolv::ResolvError, Resolv::ResolvTimeout
|
|
36
|
+
false
|
|
37
|
+
ensure
|
|
38
|
+
resolver&.close
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Retrieve MX records for a domain.
|
|
42
|
+
#
|
|
43
|
+
# @param domain [String] the domain to look up
|
|
44
|
+
# @param timeout [Integer] DNS query timeout in seconds
|
|
45
|
+
# @return [Array<String>] list of MX hostnames sorted by preference
|
|
46
|
+
def mx_records(domain, timeout: DEFAULT_TIMEOUT)
|
|
47
|
+
return [] if domain.nil? || domain.strip.empty?
|
|
48
|
+
|
|
49
|
+
resolver = Resolv::DNS.new
|
|
50
|
+
resolver.timeouts = timeout
|
|
51
|
+
|
|
52
|
+
records = fetch_mx_records(resolver, domain)
|
|
53
|
+
records.sort_by(&:preference).map { |r| r.exchange.to_s }
|
|
54
|
+
rescue Resolv::ResolvError, Resolv::ResolvTimeout
|
|
55
|
+
[]
|
|
56
|
+
ensure
|
|
57
|
+
resolver&.close
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# @param resolver [Resolv::DNS]
|
|
63
|
+
# @param domain [String]
|
|
64
|
+
# @return [Array<Resolv::DNS::Resource::IN::MX>]
|
|
65
|
+
def fetch_mx_records(resolver, domain)
|
|
66
|
+
resolver.getresources(domain, Resolv::DNS::Resource::IN::MX)
|
|
67
|
+
rescue Resolv::ResolvError, Resolv::ResolvTimeout
|
|
68
|
+
[]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param resolver [Resolv::DNS]
|
|
72
|
+
# @param domain [String]
|
|
73
|
+
# @return [Array<Resolv::DNS::Resource::IN::A>]
|
|
74
|
+
def fetch_a_records(resolver, domain)
|
|
75
|
+
resolver.getresources(domain, Resolv::DNS::Resource::IN::A)
|
|
76
|
+
rescue Resolv::ResolvError, Resolv::ResolvTimeout
|
|
77
|
+
[]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module EmailValidator
|
|
5
|
+
# Value object representing the outcome of an email validation.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# result = EmailValidator.validate("user@example.com")
|
|
9
|
+
# result.valid? # => true
|
|
10
|
+
# result.errors # => []
|
|
11
|
+
# result.warnings # => []
|
|
12
|
+
class Result
|
|
13
|
+
# @return [Array<String>] list of validation error messages
|
|
14
|
+
attr_reader :errors
|
|
15
|
+
|
|
16
|
+
# @return [Array<String>] list of non-fatal warning messages
|
|
17
|
+
attr_reader :warnings
|
|
18
|
+
|
|
19
|
+
# @param errors [Array<String>] validation error messages
|
|
20
|
+
# @param warnings [Array<String>] non-fatal warning messages
|
|
21
|
+
def initialize(errors: [], warnings: [])
|
|
22
|
+
@errors = errors.freeze
|
|
23
|
+
@warnings = warnings.freeze
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Whether the email passed all validation checks.
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def valid?
|
|
31
|
+
@errors.empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# String representation for debugging.
|
|
35
|
+
#
|
|
36
|
+
# @return [String]
|
|
37
|
+
def to_s
|
|
38
|
+
if valid?
|
|
39
|
+
'#<EmailValidator::Result valid>'
|
|
40
|
+
else
|
|
41
|
+
"#<EmailValidator::Result invalid errors=#{@errors}>"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
alias inspect to_s
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module EmailValidator
|
|
5
|
+
# RFC 5322 compliant email syntax validation.
|
|
6
|
+
#
|
|
7
|
+
# Validates local part rules, domain rules, and length limits
|
|
8
|
+
# according to the relevant RFCs (5321, 5322).
|
|
9
|
+
module Syntax
|
|
10
|
+
# Maximum total length of an email address (RFC 5321).
|
|
11
|
+
MAX_EMAIL_LENGTH = 254
|
|
12
|
+
|
|
13
|
+
# Maximum length of the local part (RFC 5321).
|
|
14
|
+
MAX_LOCAL_LENGTH = 64
|
|
15
|
+
|
|
16
|
+
# Maximum length of the domain part (RFC 5321).
|
|
17
|
+
MAX_DOMAIN_LENGTH = 253
|
|
18
|
+
|
|
19
|
+
# Maximum length of a single domain label.
|
|
20
|
+
MAX_LABEL_LENGTH = 63
|
|
21
|
+
|
|
22
|
+
# Characters allowed in the local part without quoting (RFC 5322 dot-atom).
|
|
23
|
+
LOCAL_CHAR_PATTERN = %r{\A[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+\z}
|
|
24
|
+
|
|
25
|
+
# Pattern for a valid domain label.
|
|
26
|
+
LABEL_PATTERN = /\A[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\z/
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Validate the syntax of an email address.
|
|
30
|
+
#
|
|
31
|
+
# @param email [String] the email address to validate
|
|
32
|
+
# @return [Array<String>] list of error messages (empty if valid)
|
|
33
|
+
def validate(email)
|
|
34
|
+
errors = []
|
|
35
|
+
|
|
36
|
+
return ['email must be a string'] unless email.is_a?(String)
|
|
37
|
+
|
|
38
|
+
stripped = email.strip
|
|
39
|
+
|
|
40
|
+
return ['email must not be empty'] if stripped.empty?
|
|
41
|
+
|
|
42
|
+
if stripped.length > MAX_EMAIL_LENGTH
|
|
43
|
+
errors << "email exceeds maximum length of #{MAX_EMAIL_LENGTH} characters"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
parts = stripped.split('@', -1)
|
|
47
|
+
|
|
48
|
+
return ['email must contain an @ symbol'] if parts.length < 2
|
|
49
|
+
|
|
50
|
+
return ['email must contain exactly one @ symbol'] if parts.length > 2
|
|
51
|
+
|
|
52
|
+
local, domain = parts
|
|
53
|
+
|
|
54
|
+
errors.concat(validate_local(local))
|
|
55
|
+
errors.concat(validate_domain(domain))
|
|
56
|
+
|
|
57
|
+
errors
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if an email address has valid syntax.
|
|
61
|
+
#
|
|
62
|
+
# @param email [String] the email address to check
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
def valid?(email)
|
|
65
|
+
validate(email).empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Validate the local part of an email address.
|
|
71
|
+
#
|
|
72
|
+
# @param local [String] the local part (before @)
|
|
73
|
+
# @return [Array<String>] list of error messages
|
|
74
|
+
def validate_local(local)
|
|
75
|
+
errors = []
|
|
76
|
+
|
|
77
|
+
if local.empty?
|
|
78
|
+
errors << 'local part must not be empty'
|
|
79
|
+
return errors
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if local.length > MAX_LOCAL_LENGTH
|
|
83
|
+
errors << "local part exceeds maximum length of #{MAX_LOCAL_LENGTH} characters"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
errors << 'local part must not start with a dot' if local.start_with?('.')
|
|
87
|
+
|
|
88
|
+
errors << 'local part must not end with a dot' if local.end_with?('.')
|
|
89
|
+
|
|
90
|
+
errors << 'local part must not contain consecutive dots' if local.include?('..')
|
|
91
|
+
|
|
92
|
+
errors << 'local part contains invalid characters' unless local.match?(LOCAL_CHAR_PATTERN)
|
|
93
|
+
|
|
94
|
+
errors
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Validate the domain part of an email address.
|
|
98
|
+
#
|
|
99
|
+
# @param domain [String] the domain part (after @)
|
|
100
|
+
# @return [Array<String>] list of error messages
|
|
101
|
+
def validate_domain(domain)
|
|
102
|
+
errors = []
|
|
103
|
+
|
|
104
|
+
if domain.empty?
|
|
105
|
+
errors << 'domain must not be empty'
|
|
106
|
+
return errors
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if domain.length > MAX_DOMAIN_LENGTH
|
|
110
|
+
errors << "domain exceeds maximum length of #{MAX_DOMAIN_LENGTH} characters"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
errors << 'domain must not start with a hyphen' if domain.start_with?('-')
|
|
114
|
+
|
|
115
|
+
errors << 'domain must not end with a hyphen' if domain.end_with?('-')
|
|
116
|
+
|
|
117
|
+
labels = domain.split('.')
|
|
118
|
+
|
|
119
|
+
errors << 'domain must contain at least two labels' if labels.length < 2
|
|
120
|
+
|
|
121
|
+
labels.each do |label|
|
|
122
|
+
if label.empty?
|
|
123
|
+
errors << 'domain must not contain empty labels'
|
|
124
|
+
next
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if label.length > MAX_LABEL_LENGTH
|
|
128
|
+
errors << "domain label '#{label}' exceeds maximum length of #{MAX_LABEL_LENGTH} characters"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
errors << "domain label '#{label}' contains invalid characters" unless label.match?(LABEL_PATTERN)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
tld = labels.last
|
|
135
|
+
errors << 'top-level domain must not be all numeric' if tld&.match?(/\A\d+\z/)
|
|
136
|
+
|
|
137
|
+
errors
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
require_relative 'email_validator/version'
|
|
6
|
+
require_relative 'email_validator/result'
|
|
7
|
+
require_relative 'email_validator/syntax'
|
|
8
|
+
require_relative 'email_validator/mx_check'
|
|
9
|
+
require_relative 'email_validator/disposable'
|
|
10
|
+
|
|
11
|
+
module Philiprehberger
|
|
12
|
+
module EmailValidator
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Role-based local parts that typically represent groups, not individuals.
|
|
16
|
+
ROLE_BASED_LOCALS = Set.new(%w[
|
|
17
|
+
abuse admin billing contact dev devnull ftp help hostmaster
|
|
18
|
+
info mail mailer-daemon marketing noc noreply no-reply
|
|
19
|
+
office postmaster press registrar remove root sales security
|
|
20
|
+
spam subscribe support sysadmin tech undisclosed-recipients
|
|
21
|
+
unsubscribe usenet uucp webmaster www
|
|
22
|
+
]).freeze
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Quick syntax check for an email address.
|
|
26
|
+
#
|
|
27
|
+
# @param email [String] the email address to validate
|
|
28
|
+
# @return [Boolean] true if syntax is valid
|
|
29
|
+
def valid?(email)
|
|
30
|
+
Syntax.valid?(email)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Full validation returning a Result object.
|
|
34
|
+
#
|
|
35
|
+
# @param email [String] the email address to validate
|
|
36
|
+
# @param check_mx [Boolean] whether to verify MX records (default: false)
|
|
37
|
+
# @param allow_disposable [Boolean] whether to allow disposable domains (default: true)
|
|
38
|
+
# @return [Result] validation result with errors and warnings
|
|
39
|
+
def validate(email, check_mx: false, allow_disposable: true)
|
|
40
|
+
errors = []
|
|
41
|
+
warnings = []
|
|
42
|
+
|
|
43
|
+
syntax_errors = Syntax.validate(email)
|
|
44
|
+
errors.concat(syntax_errors)
|
|
45
|
+
|
|
46
|
+
if syntax_errors.empty?
|
|
47
|
+
errors << 'disposable email domains are not allowed' if !allow_disposable && Disposable.disposable?(email)
|
|
48
|
+
|
|
49
|
+
warnings << 'address appears to be role-based' if role_based?(email)
|
|
50
|
+
|
|
51
|
+
if check_mx
|
|
52
|
+
domain = extract_domain(email)
|
|
53
|
+
errors << "domain '#{domain}' has no MX or A records" unless MxCheck.valid?(domain)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Result.new(errors: errors, warnings: warnings)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if a domain has valid MX records.
|
|
61
|
+
#
|
|
62
|
+
# @param domain [String] the domain to check
|
|
63
|
+
# @return [Boolean] true if MX or A records exist
|
|
64
|
+
def mx_valid?(domain)
|
|
65
|
+
MxCheck.valid?(domain)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check if an email address uses a known disposable domain.
|
|
69
|
+
#
|
|
70
|
+
# @param email [String] the email address to check
|
|
71
|
+
# @return [Boolean] true if the domain is disposable
|
|
72
|
+
def disposable?(email)
|
|
73
|
+
Disposable.disposable?(email)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Detect role-based email addresses (info@, admin@, support@, etc.).
|
|
77
|
+
#
|
|
78
|
+
# @param email [String] the email address to check
|
|
79
|
+
# @return [Boolean] true if the local part is role-based
|
|
80
|
+
def role_based?(email)
|
|
81
|
+
return false unless email.is_a?(String)
|
|
82
|
+
|
|
83
|
+
local = extract_local(email)
|
|
84
|
+
return false if local.nil?
|
|
85
|
+
|
|
86
|
+
ROLE_BASED_LOCALS.include?(local.downcase)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Extract the domain part from an email address.
|
|
92
|
+
#
|
|
93
|
+
# @param email [String]
|
|
94
|
+
# @return [String, nil]
|
|
95
|
+
def extract_domain(email)
|
|
96
|
+
parts = email.strip.split('@', 2)
|
|
97
|
+
return nil if parts.length != 2
|
|
98
|
+
|
|
99
|
+
parts[1]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Extract the local part from an email address.
|
|
103
|
+
#
|
|
104
|
+
# @param email [String]
|
|
105
|
+
# @return [String, nil]
|
|
106
|
+
def extract_local(email)
|
|
107
|
+
parts = email.strip.split('@', 2)
|
|
108
|
+
return nil if parts.length != 2
|
|
109
|
+
|
|
110
|
+
parts[0]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: philiprehberger-email_validator
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Philip Rehberger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Validates email addresses with RFC 5322 syntax checking, MX record verification,
|
|
14
|
+
disposable domain detection, and role-based address identification.
|
|
15
|
+
email:
|
|
16
|
+
- me@philiprehberger.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/philiprehberger/email_validator.rb
|
|
25
|
+
- lib/philiprehberger/email_validator/disposable.rb
|
|
26
|
+
- lib/philiprehberger/email_validator/mx_check.rb
|
|
27
|
+
- lib/philiprehberger/email_validator/result.rb
|
|
28
|
+
- lib/philiprehberger/email_validator/syntax.rb
|
|
29
|
+
- lib/philiprehberger/email_validator/version.rb
|
|
30
|
+
homepage: https://github.com/philiprehberger/rb-email-validator
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT
|
|
33
|
+
metadata:
|
|
34
|
+
homepage_uri: https://github.com/philiprehberger/rb-email-validator
|
|
35
|
+
source_code_uri: https://github.com/philiprehberger/rb-email-validator
|
|
36
|
+
changelog_uri: https://github.com/philiprehberger/rb-email-validator/blob/main/CHANGELOG.md
|
|
37
|
+
bug_tracker_uri: https://github.com/philiprehberger/rb-email-validator/issues
|
|
38
|
+
rubygems_mfa_required: 'true'
|
|
39
|
+
post_install_message:
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 3.1.0
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubygems_version: 3.5.22
|
|
55
|
+
signing_key:
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: RFC-compliant email validation with MX record verification
|
|
58
|
+
test_files: []
|