# frozen_string_literal: true
#
# Copyright (c) 2006-2023 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-support is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-support is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-support. If not, see .
#
require 'ronin/support/network/exceptions'
module Ronin
module Support
module Network
#
# Represents an email address.
#
# ## Features
#
# * Supports normalizing tagged emails.
# * Supports {EmailAddress.deobfuscate deobfuscating} email addresses.
# * Supports {EmailAddress#obfuscate obfuscating} email addresses.
#
# ## Examples
#
# Builds a new email address:
#
# email = EmailAddress.new(mailbox: 'john.smith', domain: 'example.com')
#
# Parses an email address:
#
# email = EmailAddress.parse("John Smith ")
# # => #
#
# Deobfuscate an obfuscated email address:
#
# EmailAddress.deobfuscate("john[dot]smith [at] example[dot]com")
# # => "john.smith@example.com"
#
# Obfuscate an email address:
#
# email = EmailAddress.parse("john.smith@example.com")
# email.obfuscate
# # => "john smith example com"
#
# Get every obfuscation of an email address:
#
# email.obfuscations
# # => ["john.smith AT example.com",
# # "john.smith at example.com",
# # "john.smith[AT]example.com",
# # "john.smith[at]example.com",
# # "john.smith [AT] example.com",
# # "john.smith [at] example.com",
# # "john.smithexample.com",
# # "john.smithexample.com",
# # "john.smith example.com",
# # "john.smith example.com",
# # "john.smith{AT}example.com",
# # "john.smith{at}example.com",
# # "john.smith {AT} example.com",
# # "john.smith {at} example.com",
# # "john.smith(AT)example.com",
# # "john.smith(at)example.com",
# # "john.smith (AT) example.com",
# # "john.smith (at) example.com",
# # "john DOT smith AT example DOT com",
# # "john dot smith at example dot com",
# # "john[DOT]smith[AT]example[DOT]com",
# # "john[dot]smith[at]example[dot]com",
# # "john [DOT] smith [AT] example [DOT] com",
# # "john [dot] smith [at] example [dot] com",
# # "johnsmithexamplecom",
# # "johnsmithexamplecom",
# # "john smith example com",
# # "john smith example com",
# # "john{DOT}smith{AT}example{DOT}com",
# # "john{dot}smith{at}example{dot}com",
# # "john {DOT} smith {AT} example {DOT} com",
# # "john {dot} smith {at} example {dot} com",
# # "john(DOT)smith(AT)example(DOT)com",
# # "john(dot)smith(at)example(dot)com",
# # "john (DOT) smith (AT) example (DOT) com",
# # "john (dot) smith (at) example (dot) com"]
#
# @see https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
#
# @api public
#
# @since 1.0.0
#
class EmailAddress
# The optional name associated with the email address
# (ex: `John Smith , nil]
attr_reader :routing
# The domain of the email address (ex: `john.smith@example.com`).
#
# @return [String, nil]
attr_reader :domain
# The IP address of the email address (ex: `john.smith@[1.2.3.4]`).
#
# @return [String, nil]
attr_reader :address
#
# Initializes the email address.
#
# @param [String, nil] name
# The optional name associated with the email address
# (ex: `John Smith , nil] routing
# Additional hosts to route sent emails through; aka the
# "percent hack" (ex: `john.smith%host3.com%host2.com@host1.com`).
#
# @param [String, nil] domain
# The domain of the email address (ex: `john.smith@example.com`).
#
# @param [String, nil] address
# The IP address of the email address (ex: `john.smith@[1.2.3.4]`).
#
# @raise [ArgumentError]
# Must specify either the `domain:` or `address:` keyword arguments.
#
# @example Initializing a basic email address.
# email = EmailAddress.new(mailbox: 'john.smith', domain: 'example.com')
#
# @example Initializing an email address with a name:
# email = EmailAddress.new(name: 'John Smith', mailbox: 'john.smith', domain: 'example.com')
#
# @example Initializing an email address with a sorting tag:
# email = EmailAddress.new(mailbox: 'john.smith', tag: 'spam', domain: 'example.com')
#
def initialize(name: nil, mailbox: , tag: nil, routing: nil, domain: nil, address: nil)
@name = name
@mailbox = mailbox
@tag = tag
@routing = routing
unless (domain.nil? ^ address.nil?)
raise(ArgumentError,"must specify domain: or address: keyword arguments")
end
@domain = domain
@address = address
end
#
# Parses an email address.
#
# @param [String] string
# The email string to parse.
#
# @return [EmailAddress]
# The parsed email address.
#
# @raise [InvalidEmailAddress]
# The string is not a valid formatted email address.
#
# @example
# email = EmailAddress.parse("John Smith ")
# # => #
#
# @see https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
#
def self.parse(string)
if string.include?('<') && string.end_with?('>')
# Name
if (match = string.match(/^([^<]+)\s+<([^>]+)>$/))
name = match[1]
address = match[2]
else
raise(InvalidEmailAddress,"invalid email address: #{string.inspect}")
end
else
name = nil
address = string
end
return new(name: name, **parse_address(address))
end
#
# Parses the address portion of an email address.
#
# @param [String] string
# the email address string to parse.
#
# @return [Hash{Symbol => Object}]
# Keyword arguments for {#initialize}.
#
# @raise [InvalidEmailAddress]
# The string did not contain a `@` character.
#
# @api private
#
def self.parse_address(string)
unless string.include?('@')
raise(InvalidEmailAddress,"invalid email address: #{string.inspect}")
end
# local-part@domain.com
local_part, domain = string.split('@',2)
return {**parse_local_part(local_part), **parse_domain(domain)}
end
#
# Parses the local-part portion of an email address.
#
# @param [String] string
# the local-part string to parse.
#
# @return [Hash{Symbol => Object}]
# Keyword arguments for {#initialize}.
#
# @api private
#
def self.parse_local_part(string)
routing = nil
if string.include?('%')
mailbox, *routing = string.split('%')
else
mailbox = string
routing = nil
end
return {routing: routing, **parse_mailbox(mailbox)}
end
#
# Parses the mailbox portion of an email address.
#
# @param [String] string
# The mailbox string to parse.
#
# @return [Hash{Symbol => Object}]
# Keyword arguments for {#initialize}.
#
# @api private
#
def self.parse_mailbox(string)
# extract any sorting-tags (ex: `user+service@domain.com`)
mailbox, tag = string.split('+',2)
return {mailbox: mailbox, tag: tag}
end
#
# Parses the domain portion of an email address.
#
# @param [String] string
# The domain portion to parse.
#
# @return [Hash{Symbol => Object}]
# Keyword arguments for {#initialize}.
#
# @api private
#
def self.parse_domain(string)
domain = nil
address = nil
# extract IP addresses from the domain part (ex: `user@[1.2.3.4]`)
if string.start_with?('[') && string.end_with?(']')
address = string[1..-2]
else
domain = string
end
return {domain: domain, address: address}
end
# Email address de-obfuscation rules.
DEOBFUSCATIONS = {
/\s+@\s+/ => '@',
/\s+(?:at|AT)\s+/ => '@',
/\s+(?:dot|DOT)\s+/ => '.',
/\s*\[(?:at|AT)\]\s*/ => '@',
/\s*\[(?:dot|DOT)\]\s*/ => '.',
/\s*\<(?:at|AT)\>\s*/ => '@',
/\s*\<(?:dot|DOT)\>\s*/ => '.',
/\s*\{(?:at|AT)\}\s*/ => '@',
/\s*\{(?:dot|DOT)\}\s*/ => '.',
/\s*\((?:at|AT)\)\s*/ => '@',
/\s*\((?:dot|DOT)\)\s*/ => '.'
}
#
# Deobfuscates an obfuscated email address.
#
# @param [String] string
# The obfuscated email address to deobfuscate.
#
# @return [String]
# The deobfuscated email address.
#
# @example
# EmailAddress.deobfuscate("john[dot]smith [at] example[dot]com")
# # => "john.smith@example.com"
#
def self.deobfuscate(string)
DEOBFUSCATIONS.each do |pattern,replace|
string = string.gsub(pattern,replace)
end
return string
end
#
# Creates a new email address without the {#tag} or {#routing}
# attribute.
#
# @return [EmailAddress]
# The new normalized email address object.
#
# @example
# email = EmailAddress.parse("John Smith ")
# email.normalize.to_s
# # => "john.smith@example.com"
#
def normalize
EmailAddress.new(
mailbox: mailbox,
domain: domain,
address: address
)
end
#
# The hostname to connect to.
#
# @return [String]
# The {#domain} or {#address}.
#
def hostname
@domain || @address
end
#
# Converts the email address back into a string.
#
# @return [String]
# The string representation of the email address.
#
# @example
# email = EmailAddress.parse("John Smith ")
# email.to_s
# # => "John Smith "
#
def to_s
string = "#{@mailbox}"
string << "+#{@tag}" if @tag
string << "%#{@routing.join('%')}" if @routing
string << "@"
string << if @address then "[#{@address}]"
else @domain
end
string = "#{@name} <#{string}>" if @name
return string
end
alias to_str to_s
#
# @group Obfuscation Methods
#
# Email address obfuscation rules.
OBFUSCATIONS = [
[/\@/, {'@' => ' @ ' }],
[/\@/, {'@' => ' AT ' }],
[/\@/, {'@' => ' at ' }],
[/\@/, {'@' => '[AT]' }],
[/\@/, {'@' => '[at]' }],
[/\@/, {'@' => ' [AT] '}],
[/\@/, {'@' => ' [at] '}],
[/\@/, {'@' => '' }],
[/\@/, {'@' => '' }],
[/\@/, {'@' => ' '}],
[/\@/, {'@' => ' '}],
[/\@/, {'@' => '{AT}' }],
[/\@/, {'@' => '{at}' }],
[/\@/, {'@' => ' {AT} '}],
[/\@/, {'@' => ' {at} '}],
[/\@/, {'@' => '(AT)' }],
[/\@/, {'@' => '(at)' }],
[/\@/, {'@' => ' (AT) '}],
[/\@/, {'@' => ' (at) '}],
[/[\.\@]/, {'.' => ' DOT ', '@' => ' AT ' }],
[/[\.\@]/, {'.' => ' dot ', '@' => ' at ' }],
[/[\.\@]/, {'.' => '[DOT]', '@' => '[AT]' }],
[/[\.\@]/, {'.' => '[dot]', '@' => '[at]' }],
[/[\.\@]/, {'.' => ' [DOT] ', '@' => ' [AT] '}],
[/[\.\@]/, {'.' => ' [dot] ', '@' => ' [at] '}],
[/[\.\@]/, {'.' => '', '@' => '' }],
[/[\.\@]/, {'.' => '', '@' => '' }],
[/[\.\@]/, {'.' => ' ', '@' => ' '}],
[/[\.\@]/, {'.' => ' ', '@' => ' '}],
[/[\.\@]/, {'.' => '{DOT}', '@' => '{AT}' }],
[/[\.\@]/, {'.' => '{dot}', '@' => '{at}' }],
[/[\.\@]/, {'.' => ' {DOT} ', '@' => ' {AT} '}],
[/[\.\@]/, {'.' => ' {dot} ', '@' => ' {at} '}],
[/[\.\@]/, {'.' => '(DOT)', '@' => '(AT)' }],
[/[\.\@]/, {'.' => '(dot)', '@' => '(at)' }],
[/[\.\@]/, {'.' => ' (DOT) ', '@' => ' (AT) '}],
[/[\.\@]/, {'.' => ' (dot) ', '@' => ' (at) '}]
]
#
# Obfuscates the email address.
#
# @return [String]
# A randomly obfuscated version of the email address.
#
# @see OBFUSCATIONS
#
# @example
# email = EmailAddress.parse("john.smith@example.com")
# email.obfuscate
# # => "john.smith [AT] example.com"
# email.obfuscate
# # => "john smith example com"
#
def obfuscate
string = to_s
string.gsub!(*OBFUSCATIONS.sample)
return string
end
#
# Enumerates over each obfuscation of the email address.
#
# @yield [obfuscated]
# If a block is given, it will be passed every obfuscation of the
# email address.
#
# @yieldparam [String] obfuscated
# An obfuscated version of the email address.
#
# @return [Enumerator]
# If no block is given, an Enumerator will be returned.
#
# @example
# email = EmailAddress.parse("john.smith@example.com")
# email.each_obfuscation { |obfuscated_email| ... }
#
# @see OBFUSCATIONS
#
def each_obfuscation
return enum_for(__method__) unless block_given?
string = to_s
OBFUSCATIONS.each do |gsub_args|
yield string.gsub(*gsub_args)
end
return nil
end
#
# Returns every obfuscation of the email address.
#
# @return [Array]
# The Array containing every obfuscation of the email address.
#
# @example
# email = EmailAddress.parse("john.smith@example.com")
# email.obfuscations
# # => ["john.smith AT example.com",
# # "john.smith at example.com",
# # "john.smith[AT]example.com",
# # "john.smith[at]example.com",
# # "john.smith [AT] example.com",
# # "john.smith [at] example.com",
# # "john.smithexample.com",
# # "john.smithexample.com",
# # "john.smith example.com",
# # "john.smith example.com",
# # "john.smith{AT}example.com",
# # "john.smith{at}example.com",
# # "john.smith {AT} example.com",
# # "john.smith {at} example.com",
# # "john.smith(AT)example.com",
# # "john.smith(at)example.com",
# # "john.smith (AT) example.com",
# # "john.smith (at) example.com",
# # "john DOT smith AT example DOT com",
# # "john dot smith at example dot com",
# # "john[DOT]smith[AT]example[DOT]com",
# # "john[dot]smith[at]example[dot]com",
# # "john [DOT] smith [AT] example [DOT] com",
# # "john [dot] smith [at] example [dot] com",
# # "johnsmithexamplecom",
# # "johnsmithexamplecom",
# # "john smith example com",
# # "john smith example com",
# # "john{DOT}smith{AT}example{DOT}com",
# # "john{dot}smith{at}example{dot}com",
# # "john {DOT} smith {AT} example {DOT} com",
# # "john {dot} smith {at} example {dot} com",
# # "john(DOT)smith(AT)example(DOT)com",
# # "john(dot)smith(at)example(dot)com",
# # "john (DOT) smith (AT) example (DOT) com",
# # "john (dot) smith (at) example (dot) com"]
#
# @see #each_obfuscation
#
def obfuscations
each_obfuscation.to_a
end
end
end
end
end