Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions lib/valid_email2/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Address
PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'#`]/
DEFAULT_RECIPIENT_DELIMITER = '+'
DOT_DELIMITER = '.'
HYPHEN_DELIMITER = '-'
MAX_DOMAIN_LENGTH = 253
MAX_LABEL_LENGTH = 63

def self.prohibited_domain_characters_regex
@prohibited_domain_characters_regex ||= PROHIBITED_DOMAIN_CHARACTERS_REGEX
Expand Down Expand Up @@ -52,13 +55,13 @@ def valid?
def valid_domain?
domain = address.domain
return false if domain.nil?
return false if domain =~ self.class.prohibited_domain_characters_regex
return false if domain.length > MAX_DOMAIN_LENGTH

domain !~ self.class.prohibited_domain_characters_regex &&
domain.include?('.') &&
!domain.include?('..') &&
!domain.start_with?('.') &&
!domain.start_with?('-') &&
!domain.include?('-.')
labels = domain.split(DOT_DELIMITER, -1)
return false if labels.length < 2

labels.all? { |label| valid_domain_label?(label) }
end

def valid_address?
Expand Down Expand Up @@ -109,6 +112,14 @@ def valid_strict_mx?

private

# An RFC 1035 label: 1-63 octets, and it may not begin or end with a hyphen.
def valid_domain_label?(label)
!label.empty? &&
label.length <= MAX_LABEL_LENGTH &&
!label.start_with?(HYPHEN_DELIMITER) &&
!label.end_with?(HYPHEN_DELIMITER)
end

def disposable_mx_server?
mx_server_is_in?(ValidEmail2.disposable_emails)
end
Expand Down
42 changes: 42 additions & 0 deletions spec/valid_email2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,48 @@ class TestUserMultiple < TestModel
user = TestUser.new(email: "foo@example.com#")
expect(user.valid?).to be_falsey
end

describe "domain label structure" do
# A domain label may not begin or end with a hyphen and is limited to 63
# octets; the whole domain is limited to 253. These rules apply to every
# label, not only the first/last position of the domain string.
long_label = "a" * 64
long_domain = (["aa"] + ["a"] * 126).join(".") # 254 chars, all-valid labels

{
"a leading hyphen on the first label" => "foo@-example.com",
"a leading hyphen on a later label" => "foo@sub.-example.com",
"a leading hyphen on a deeply nested label" => "foo@a.b.-c.example.com",
"a trailing hyphen on a non-final label" => "foo@example-.com",
"a trailing hyphen on the final label" => "foo@example.com-",
"a bare hyphen label" => "foo@example.-.com",
"a label longer than 63 octets" => "foo@#{long_label}.com",
"a later label longer than 63 octets" => "foo@example.#{long_label}.com",
"a domain longer than 253 octets" => "foo@#{long_domain}"
}.each do |description, email|
it "is invalid with #{description}" do
expect(TestUser.new(email: email).valid?).to be_falsey
end
end

full_label = "a" * 63
max_domain = (["a"] * 127).join(".") # exactly 253 chars

{
"an internal hyphen" => "foo@ex-ample.com",
"hyphens on several internal labels" => "foo@f-o-o.example.com",
"a double internal hyphen" => "foo@a--b.example.com",
"a punycode (IDNA ACE) label" => "foo@xn--bcher-kva.com",
"a label beginning with a digit" => "foo@3m.com",
"a 63 octet label at the limit" => "foo@#{full_label}.com",
"a later 63 octet label" => "foo@example.#{full_label}.com",
"a domain at the 253 octet limit" => "foo@#{max_domain}"
}.each do |description, email|
it "is valid with #{description}" do
expect(TestUser.new(email: email).valid?).to be_truthy
end
end
end
end

describe "with disposable validation" do
Expand Down