Skip to content

Commit

Permalink
logo + fix
Browse files Browse the repository at this point in the history
  • Loading branch information
balestek committed Apr 26, 2024
1 parent ea07540 commit ea88590
Show file tree
Hide file tree
Showing 10 changed files with 60 additions and 47 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# hashtray

<p align="center">
<img src="https://raw.githubusercontent.com/balestek/hashtray/master/media/hashtray.png">
<img src="https://raw.githubusercontent.com/balestek/hashtray/master/media/hashtray-logo.png">
</p>

[![PyPI version](https://badge.fury.io/py/hashtray.svg)](https://badge.fury.io/py/hashtray)
Expand Down Expand Up @@ -67,6 +67,10 @@ pip install hashtray

### Find Gravatar account with an email

<p align="center">
<img src="https://raw.githubusercontent.com/balestek/hashtray/master/media/hashtray-email.png">
</p>

Pretty straightforward. The command is `email` .

It converts the email address into its MD5 hash. _hashtray_ then checks if a public profile associated with the hash exists on Gravatar. If found, it displays the profile information.
Expand All @@ -81,6 +85,10 @@ In such cases, _hashtray_ alerts you. You can then attempt to find the primary e

### Find an email from a Gravatar username or hash

<p align="center">
<img src="https://raw.githubusercontent.com/balestek/hashtray/master/media/hashtray-account.png">
</p>

To find an email address associated with a Gravatar username or hash, use the account command.

hashtray creates a list of possible email addresses using data from the Gravatar profile.
Expand Down
2 changes: 1 addition & 1 deletion hashtray/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.1b4"
__version__ = "0.0.1"
24 changes: 12 additions & 12 deletions hashtray/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from rich.console import Console

from hashtray.__about__ import __version__ as version
from hashtray.email_enum import EmailEnum
from hashtray.gravatar import Gravatar

Expand Down Expand Up @@ -61,20 +62,19 @@ def parse_app_args(arguments=None):

def main() -> None:
c.print(
r"""
██╗░░██╗░█████╗░░██████╗██╗░░██╗████████╗██████╗░░█████╗░██╗░░░██╗
██║░░██║██╔══██╗██╔════╝██║░░██║╚══██╔══╝██╔══██╗██╔══██╗╚██╗░██╔╝
███████║███████║╚█████╗░███████║░░░██║░░░██████╔╝███████║░╚████╔╝░
██╔══██║██╔══██║░╚═══██╗██╔══██║░░░██║░░░██╔══██╗██╔══██║░░╚██╔╝░░
██║░░██║██║░░██║██████╔╝██║░░██║░░░██║░░░██║░░██║██║░░██║░░░██║░░░
╚═╝░░╚═╝╚═╝░░╚═╝╚═════╝░╚═╝░░╚═╝░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚═╝░░░╚═╝░░░
[white]j m . b a l e s t e k[/white]
f"""[bold]
[turquoise2]⠀⠀⣠⣴⣶⠿⠿⣷⣶⣄⠀⠀⠀[/turquoise2]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
[turquoise2]⠀⢾⠟⠉⠀⠀⠀⠀⠈⠁⠀⠀⠀⠀[/turquoise2]⣤⡄⠀⣤⠀⠀⣤⣄⠀⢀⣤⠤⠤⠀⣤⠀⢠⡄⠤⢤⣤⠤⢠⣤⠤⣤⠀⠀⣠⣤⠀⠠⣤⠀⢠⡄
[turquoise2]⠀⠀⠀⠀⠀⠀⣶⣶⣶⣶⣶⣶⠂⠀[/turquoise2]⣿⣇⣀⣿⠀⢰⡟⢿⡀⠸⣧⣀⠀⠀⣿⣀⣸⡇⠀⢸⡇⠀⢸⡇⠀⣹⡇⢀⣿⢻⡇⠀⢹⣦⡿⠁
[turquoise2]⣀⣀⠀⠀⠀⠀⠋⠃⠀⠀⢸⣿⠀⠀[/turquoise2]⣿⡏⠉⣿⠀⣾⠧⢾⣇⠀⠈⠙⣿⠀⣿⠉⢹⡇⠀⢸⡇⠀⢸⡿⢺⣟⠀⣸⡷⠼⣿⠀⠀⣿⠃⠀
[turquoise2]⠘⢿⣦⣀⠀⠀⠀⠀⢀⣴⣿⠋⠀⠀[/turquoise2]⠛⠃⠀⠛⠐⠛⠀⠀⠛⠐⠶⠶⠛⠀⠛⠀⠘⠃⠀⠘⠃⠀⠘⠓⠀⠛⠂⠛⠀⠀⠙⠃⠀⠛⠀⠀
[turquoise2]⠀⠀⠙⠻⠿⣶⣶⡿⠿⠋⠁ [/turquoise2][/bold]jm balestek⠀v{version}⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀
""",
style="#1e8cbe",
)
c.print(
"[bold turquoise2]Gravatar Account and Email Finder[/bold turquoise2]\n"
"Find a Gravatar account from an email address or find an email address from a Gravatar account or hash.\n"
"Find a Gravatar account from an email address or\n"
"find an email address from a Gravatar account or hash.\n"
"\n"
":arrow_forward: [bold turquoise2]Find a gravatar account from en email:[/bold turquoise2]\n"
" [bright_white]Usage:[/bright_white] [gold1]hashtray[/gold1] [orange_red1]email[/orange_red1] [email protected]\n"
Expand All @@ -85,9 +85,9 @@ def main() -> None:
" [bright_white]Options:[/bright_white]\n"
" [orange3]--domain_list, -l[/orange3] [tan]common|long|full[/tan]\n"
" Domain list to use for email enumeration. Default: common\n"
" [orange3]--elements, -e[/orange3] [tan]element1 element2,...[/tan]\n"
" [orange3]--elements, -e[/orange3] [tan]element1 element2 ...[/tan]\n"
" Generate combinations with your elements/strings instead\n"
" [orange3]--domains, -d[/orange3] [tan]domain1.com domain2.com,...[/tan]\n"
" [orange3]--domains, -d[/orange3] [tan]domain1.com domain2.com ...[/tan]\n"
" Use your custom email domains for emails generation\n"
" [orange3]--crazy, -c[/orange3] Go crazy and try EVERY SINGLE combination\n"
" (with any special character at any place in the combinations)\n"
Expand Down
2 changes: 1 addition & 1 deletion hashtray/data/email_services_full.json
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@
"vnn.vn",
"aver.com",
"bestweb.net",
"chickenkiller.com"
"chickenkiller.com",
"keemail.com",
"posteo.us",
"posteo.uk",
Expand Down
58 changes: 31 additions & 27 deletions hashtray/email_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ def __init__(
custom_domains: list = None,
crazy: bool = False,
):
self.account_hash = self.hashed = account_hash
self.account_hash = account_hash
self.hashed = None
self.separators = ["", ".", "_", "-"]
self.name_pattern = "[-_ ./]"
self.emails = []
Expand All @@ -42,7 +43,7 @@ def __init__(
self.n_combs = 0
self.c = c(highlight=False)

def load_domains(self):
def load_domains(self) -> json:
# Load domains from json files
domain_files = {None: "", "common": "", "long": "_long", "full": "_full"}
domain_file = domain_files[self.domain_list]
Expand All @@ -65,13 +66,14 @@ def create_elements(self) -> list:
)
sys.exit(1)
# Get elements with custom arguments or from the Gravatar profile
self.hashed = self.g.info()["hash"]
if self.elements:
elements = self.get_custom_elements()
else:
elements = self.get_elements_from_gravatar()
return elements

def get_custom_elements(self):
def get_custom_elements(self) -> list:
# Get elements from custom arguments
self.elements = [
unidecode(element.lower())
Expand All @@ -85,19 +87,18 @@ def get_custom_elements(self):
)
return elements

def get_elements_from_gravatar(self):
def get_elements_from_gravatar(self) -> list:
# Get elements from the Gravatar profile
elements = []
infos = self.g.info()
self.get_public_emails(infos)
self.hashed = infos["hash"]
gob = self.process_gravatar_info(infos)
for element in gob:
deco = unidecode(element.lower())
elements.append(deco) if deco not in elements else None
return elements

def get_public_emails(self, infos):
def get_public_emails(self, infos: dict) -> None:
# Get emails from the Gravatar json emails
if infos["emails"]:
self.public_emails.extend(
Expand All @@ -110,7 +111,7 @@ def get_public_emails(self, infos):
find = re.findall(pattern, infos["aboutMe"]) if infos["aboutMe"] else None
self.public_emails.extend(find) if find else None

def process_gravatar_info(self, infos):
def process_gravatar_info(self, infos: dict) -> list:
# Process Gravatar infos to get additional chunks
gob = []
self.add_preferred_username(infos, gob)
Expand All @@ -122,44 +123,44 @@ def process_gravatar_info(self, infos):
self.add_elements(gob)
return gob

def add_preferred_username(self, infos, gob):
def add_preferred_username(self, infos: dict, gob: list) -> None:
if infos["preferredUsername"]:
gob.append(infos["preferredUsername"])

def add_profile_url(self, infos, gob):
def add_profile_url(self, infos: dict, gob: list) -> None:
if infos["profileUrl"]:
gob.append(self.last_url_chunk(infos["profileUrl"]))

def add_chunks(self, string, gob):
def add_chunks(self, string: str, gob: list) -> None:
if string:
names = re.split(self.name_pattern, unidecode(string))
chunks = [name for name in names if name]
# Add first letter for given and family names
chunks.extend(name[:1] for name in names if len(name) > 1)
gob.extend(chunks)

def add_display_name(self, infos, gob):
def add_display_name(self, infos: dict, gob: list) -> None:
names = re.split(self.name_pattern, unidecode(infos["displayName"]))
gob.extend(name for name in names if name)

def add_given_name(self, infos, gob):
def add_given_name(self, infos: dict, gob: list) -> None:
# Add given name and first letter chunks
if infos["name"] and infos["name"]["givenName"]:
self.add_chunks(infos["name"]["givenName"], gob)

def add_family_name(self, infos, gob):
def add_family_name(self, infos: dict, gob: list) -> None:
# Add family name and first letter chunks
if infos["name"] and infos["name"]["familyName"]:
self.add_chunks(infos["name"]["familyName"], gob)

def add_accounts(self, infos, gob):
def add_accounts(self, infos: dict, gob: list) -> None:
# Add account chunks for verified accounts
if infos["accounts"]:
for account in infos["accounts"]:
account_url = infos["accounts"][account].rstrip("/")
self.process_account(account, account_url, gob)

def process_account(self, account, account_url, gob):
def process_account(self, account: str, account_url: str, gob: list) -> None:
# Verified accounts username chunks
if account in ["Mastodon", "Fediverse"]:
gob.append(self.last_url_chunk(account_url).replace("@", ""))
Expand Down Expand Up @@ -200,7 +201,7 @@ def process_account(self, account, account_url, gob):
chunk = self.last_url_chunk(account_url)
gob.append(chunk)

def add_elements(self, gob):
def add_elements(self, gob: list) -> None:
# Building final list of chunks, deduped , lowercase and unidecoded if it hasn't been yet
self.elements = []
self.elements = [
Expand All @@ -209,27 +210,27 @@ def add_elements(self, gob):
if unidecode(element.lower()) not in self.elements
]

def hash_email(self, email):
def hash_email(self, email: str) -> str:
# MD5 hashing of a string email
return hashlib.md5(email.lower().encode()).hexdigest()

def check_mailhash(self, s: str):
def check_mailhash(self, s: str) -> bool:
# Check if a string is a valid MD5 hash
return re.fullmatch(r"[a-fA-F0-9]{32}", s) is not None

def check_email(self, email):
def check_email(self, email: str) -> bool:
# Check if a string is a valid email
return (
True
if re.match(r"(^[a-zA-Z0-9_.%+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email)
else False
)

def last_url_chunk(self, s: str):
def last_url_chunk(self, s: str) -> str:
# Get the last chunk of an URL
return s.split("/")[-1]

def is_combination(self, s, chunks):
def is_combination(self, s: str, chunks: list) -> bool:
# Logic to check if a string is a combination of other strings
if s in chunks:
chunks.remove(s)
Expand All @@ -244,18 +245,18 @@ def is_combination(self, s, chunks):
return True
return False

def dedup_chunks(self, chunks):
def dedup_chunks(self, chunks: list) -> list:
# Remove chunks from a list of chunks if it's made with other strings of the list
return [s for s in chunks if not self.is_combination(s, chunks.copy())]

def show_chunks(self, elements):
def show_chunks(self, elements: list) -> str:
# Show chunks as a string
em = ""
for element in elements:
em += element + ", "
return em.rstrip(", ")

def get_combination_count(self, n):
def get_combination_count(self, n: int) -> int:
# Calculate the total number of combinations for tdqm bar progress
total = 0
for r in range(1, n + 1):
Expand All @@ -279,7 +280,7 @@ def get_combination_count(self, n):
# Multiply by the number of domains
return total * len(self.domains)

def combinator(self):
def combinator(self) -> str:
# Generate all possible email combinations for unique elements
# Get chunks and dedup them if made with other chunks
elements = self.dedup_chunks(self.create_elements())
Expand Down Expand Up @@ -323,7 +324,7 @@ def combinator(self):
email_local_part = separator.join(permutation)
yield f"{email_local_part}@{domain}"

def hashes(self):
def hashes(self) -> str:
# Calculate emails hash
for email in self.combinator():
hashd = self.hash_email(email)
Expand All @@ -337,7 +338,7 @@ def hashes(self):
return email
self.bar.close()

def find(self):
def find(self) -> None:
# Print process and results
self.c.print(
f"[turquoise2]Finding email for [bold]{self.account_hash}[/bold][/turquoise2]\n"
Expand Down Expand Up @@ -382,5 +383,8 @@ def find(self):
"\n[light_pink1]:question_mark: Do you want to display the profile info? (y/n):[/light_pink1]"
)
if show_profile.lower() == "y":
print(
f"------------------------------------------------------------------------------------\n"
)
self.g.print_info()
exit(0)
11 changes: 6 additions & 5 deletions hashtray/gravatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ def __init__(self, email=None, ghash: str = None, account: str = None):
highlight=False, theme=Theme({"repr.url": "not underline bold white"})
)

def check_email(self):
def check_email(self) -> bool:
# Check if email is valid
if re.match(r"(^[a-zA-Z0-9_.%+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", self.email):
return True
else:
self.c.print(f"[red]Invalid email address: {self.email}.[/red]")
exit(0)

def get_json(self):
def get_json(self) -> dict:
# Get Gravatar json data
try:
res = httpx.get(self.account_url + ".json")
Expand All @@ -91,7 +91,7 @@ def get_json(self):
except Exception as e:
print(e)

def process_list(self, info, data):
def process_list(self, info: list, data: dict) -> dict:
# Build a dictionary with the json data
info_dict = {}
for i, item in enumerate(data[info]):
Expand All @@ -113,7 +113,7 @@ def process_list(self, info, data):
info_dict[key] = item[key]
return info_dict

def info(self):
def info(self) -> dict:
# Get the Gravatar info if it's a list or not
try:
data = self.get_json()["entry"][0]
Expand All @@ -134,11 +134,12 @@ def info(self):
self.infos[info] = data[info]
return self.infos

def print_info(self):
def print_info(self) -> None:
# Print the Gravatar profile infos
data = self.info()
if self.email:
self.c.print(
f"------------------------------------------------------------------------------------\n"
f"Gravatar info for [bold white]{self.email}[/bold white]:\n"
)
for info in data:
Expand Down
Binary file added media/hashtray-account.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added media/hashtray-email.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added media/hashtray-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed media/hashtray.png
Binary file not shown.

0 comments on commit ea88590

Please sign in to comment.