Skip to content

Instantly share code, notes, and snippets.

@troelskn
Created September 30, 2020 10:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save troelskn/500c0132712e806bcf0b40d2677e0249 to your computer and use it in GitHub Desktop.
Save troelskn/500c0132712e806bcf0b40d2677e0249 to your computer and use it in GitHub Desktop.
Validate email via smtp
class EmailValidation
SMTP_PORT = 25
CONNECTION_TIMEOUT = 3
READ_TIMEOUT = 3
REGEX_SMTP_ERROR_BODY_PATTERN = /(?=.*550)(?=.*(user|account|customer|mailbox)).*/i.freeze
def initialize(verifier_domain:, verifier_email:)
@verifier_domain, @verifier_email = verifier_domain, verifier_email
end
# Probe if email is valid on the remote mx
# Largely salvaged from: https://github.com/rubygarage/truemail
def smtp_valid?(email)
punycode_email = punycode_encode(email)
host = resolve_mx_exchange(email)
# Look if server responds
Timeout.timeout(CONNECTION_TIMEOUT) do
TCPSocket.new(host, SMTP_PORT).close
end
# Then establish smtp-handshake
connection = Net::SMTP.new(host, SMTP_PORT).tap do |settings|
settings.open_timeout = CONNECTION_TIMEOUT
settings.read_timeout = READ_TIMEOUT
end
responses = []
connection.start do |request|
responses << request.public_send(:helo, @verifier_domain)
responses << request.public_send(:mailfrom, @verifier_email)
responses << request.public_send(:rcptto, punycode_email)
end
# Verify all responses, to look for an error
responses.none? { |response| response.match? REGEX_SMTP_ERROR_BODY_PATTERN }
rescue Timeout::Error, EOFError => err
# The remote server might hang up on us. Or we may not be able to reach it at all.
# In those cases, the response is undetermined. We treat this as the address being
# valid, even though we don't really know.
true
rescue RuntimeError => err
false
end
def punycode_encode(email)
return unless email.is_a?(String)
return email if email.ascii_only?
user, domain = email.split('@')
"#{user}@#{SimpleIDN.to_ascii(domain.downcase)}"
end
def resolve_mx_exchange(email)
email_address = Mail::Address.new(email)
Resolv::DNS.open do |dns|
dns.getresources(email_address.domain, Resolv::DNS::Resource::IN::MX).first.exchange.to_s
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment