diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b38df29 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" diff --git a/.gitignore b/.gitignore index ed8ebf5..38667da 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,203 @@ -__pycache__ \ No newline at end of file +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f5e4817 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + // Linter settings + "[python]": { + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.defaultFormatter": "charliermarsh.ruff" + }, + + // Files view + "files.autoSave": "afterDelay", + "files.exclude": { + "**/__pycache__": true, + "**/.venv": true, + "**/.ruff_cache": true, + "**/.mypy_cache": true, + ".python-version": true, + "uv.lock": true, + }, + + // Editor + "editor.rulers": [88], +} diff --git a/LICENSE b/LICENSE index 63b4b68..2063c1e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2025 MainSilent, iamlostshe Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index a27a4bb..a986873 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,59 @@ -# MailTM API Wrapper +# mail-tm API wrapper -[![Downloads](https://pepy.tech/badge/mailtm)](https://pepy.tech/project/mailtm) +> Fork of [MainSilent/MailTm](https://github.com/MainSilent/MailTm). -[![Downloads](https://pepy.tech/badge/mailtm/month)](https://pepy.tech/project/mailtm) -[![Downloads](https://pepy.tech/badge/mailtm/week)](https://pepy.tech/project/mailtm) +> There are plans to write a custom server for deploying personal temporary email on your server. -MailTm is a free temporary mail service, This library is useful for automation tasks such as making accounts that needs email verification. +Mail-tm is a free temporary mail service, This library is useful for automation tasks such as making accounts that needs email verification. ## Installation -Windows: - -``` -pip install MailTm -``` - -Linux/Mac OS: - -``` -pip3 install MailTm +```bash +pip install git+https://github.com/iamlostshe/mail-tm ``` ## Example ```python +import asyncio + from mailtm import Email -def listener(message): - print("\nSubject: " + message['subject']) - print("Content: " + message['text'] if message['text'] else message['html']) -# Get Domains -test = Email() -print("\nDomain: " + test.domain) +async def main() -> None: + """Start msg cycle.""" + + def listener(message: dict) -> None: + print("\nSubject: " + message["subject"]) + print("Content: " + message["text"] if message["text"] else message["html"]) + + # Get base url and domains + test = Email() + await test.init() + print("\nBase url:", test.base_url) + print("Domain:", test.domain) -# Make new email address -test.register() -print("\nEmail Adress: " + str(test.address)) + # Make new email address + await test.register() + print("\nEmail Adress:", test.address) -# Start listening -test.start(listener) -print("\nWaiting for new emails...") + # Start listening + await test.start(listener) + print("\nWaiting for new emails...") + + +if __name__ == "__main__": + asyncio.run(main()) ``` # Documentation -API: https://mail.tm +API: [api.mail.tm](https://api.mail.tm/) + +- `register(username: str | None = username_gen(), password: str | None = password_gen(), domain: str | None = None)`: -`register(username=None, password=None, domain=None)` | Make an email account with random credentials, You can also pass a username, password and domain to use the same account. +Make an email account with random credentials, You can also pass a username, password and domain to use the same account. -`start(listener, interval=3)` | Start listening for new emails, Interval means how many seconds takes to sync, And you also need to pass a function for `listener`, This function gets called when new email arrive. +- `start(listener: any, interval: int = 3) -> None`: -`stop()` | Stop listening for new emails. +Start listening for new emails, Interval means how many seconds takes to sync, And you also need to pass a function for `listener`, This function gets called when new email arrive. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1e2ac85 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "mail-tm" +version="0.0.90" +description="Temporary Email" +readme = "README.md" +requires-python = ">=3.13" +authors = [ + {name="MainSilent"}, + {name="iamlostshe", email="vanamelcikov7275@gmail.com"} +] +dependencies = [ + "aiohttp>=3.12.14", +] +keywords=['mail', 'email', 'temporary mail', 'temporary email', 'mailtm'] +classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +Homepage = "https://github.com/iamlostshe/mail-tm" +Repository = "https://github.com/iamlostshe/mail-tm.git" +"Bug Tracker" = "https://github.com/iamlostshe/mail-tm/issues" + +[tool.ruff.lint] +select = ["ALL"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 332a2fb..0000000 --- a/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="MailTm", - version="0.0.81", - author="MainSilent", - description="Temporary Email", - long_description=long_description, - long_description_content_type="text/markdown", - keywords=['mail', 'email', 'temporary mail', 'temporary email', 'mailtm'], - url="https://github.com/MainSilent/MailTm", - project_urls={ - "Bug Tracker": "https://github.com/MainSilent/MailTm/issues", - }, - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], - package_dir={"": "src"}, - packages=setuptools.find_packages(where="src"), - python_requires=">=3.6", - install_requires=['requests'] -) diff --git a/src/mailtm/__init__.py b/src/mailtm/__init__.py index 845c041..d0b8ce8 100644 --- a/src/mailtm/__init__.py +++ b/src/mailtm/__init__.py @@ -1 +1,5 @@ -from .email import Email \ No newline at end of file +"""Initializing the library.""" + +from .email import Email + +__all__ = ("Email",) diff --git a/src/mailtm/consts.py b/src/mailtm/consts.py new file mode 100644 index 0000000..de8c005 --- /dev/null +++ b/src/mailtm/consts.py @@ -0,0 +1,9 @@ +"""Some constant values.""" + +BASE_URLS = ["https://api.mail.tm/", "https://api.mail.gw/"] + +USERNAME_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789" +PASSWORD_ALPHABET = ( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" # noqa: S105 + "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" +) diff --git a/src/mailtm/email.py b/src/mailtm/email.py index 94bf592..24741d8 100644 --- a/src/mailtm/email.py +++ b/src/mailtm/email.py @@ -1,97 +1,80 @@ -import json -import string +"""The main module for interacting with EMail.""" + import random -import requests + +from aiohttp import ClientSession + +from .consts import BASE_URLS, PASSWORD_ALPHABET, USERNAME_ALPHABET from .message import Listen -def username_gen(length=24, chars= string.ascii_letters + string.digits): - return ''.join(random.choice(chars) for _ in range(length)) -def password_gen(length=8, chars= string.ascii_letters + string.digits + string.punctuation): - return ''.join(random.choice(chars) for _ in range(length)) +def username_gen( + length: int = 24, + chars: str = USERNAME_ALPHABET, +) -> str: + """Username generation.""" + return "".join(random.choice(chars) for _ in range(length)) # noqa: S311 + + +def password_gen( + length: int = 8, + chars: str = PASSWORD_ALPHABET, +) -> str: + """Password generation.""" + return "".join(random.choice(chars) for _ in range(length)) # noqa: S311 + class Email(Listen): - token = "" - domain = "" - address = "" - session = requests.Session() - - def __init__(self): - if not self.domains(): - print("Failed to get domains") - - def domains(self): - url = "https://api.mail.tm/domains" - response = self.session.get(url) - response.raise_for_status() - - try: - data = response.json() - for domain in data['hydra:member']: - if domain['isActive']: - self.domain = domain['domain'] - return True - - raise Exception("No Domain") - except: - return False - - def register(self, username=None, password=None, domain=None): - self.domain = domain if domain else self.domain - username = username if username else username_gen() - password = password if password else password_gen() - - url = "https://api.mail.tm/accounts" + """The class module for interacting with EMail.""" + + async def init(self, base_url: str | None = random.choice(BASE_URLS)) -> None: # noqa: S311 + """Init temporary Email class.""" + self.base_url = base_url + self.session = ClientSession( + base_url=self.base_url, + headers={"Content-Type": "application/json"}, + ) + await self.domains() + + async def domains(self) -> None: + """Init Email domains.""" + url = "domains" + + async with self.session.get(url) as response: + data = (await response.json())["hydra:member"] + + self.domain = random.choice([d for d in data if d["isActive"]])["domain"] # noqa: S311 + + async def register( + self, + username: str | None = username_gen(), + password: str | None = password_gen(), + domain: str | None = None, + ) -> None: + """Mail registration.""" + if domain: + self.domain = domain + + self.address = f"{username}@{self.domain}" + + url = "accounts" payload = { - "address": f"{username}@{self.domain}", - "password": password + "address": self.address, + "password": password, } - headers = { 'Content-Type': 'application/json' } - response = self.session.post(url, headers=headers, json=payload) - response.raise_for_status() - - data = response.json() - try: - self.address = data['address'] - except: - self.address = f"{username}@{self.domain}" - - self.get_token(password) - if not self.address: - raise Exception("Failed to make an address") + await self.session.post(url, json=payload) + await self.get_token(password) - def get_token(self, password): - url = "https://api.mail.tm/token" + async def get_token(self, password: str) -> None: + """Get token.""" + url = "token" payload = { "address": self.address, - "password": password + "password": password, } - headers = {'Content-Type': 'application/json'} - response = self.session.post(url, headers=headers, json=payload) - response.raise_for_status() - try: - self.token = response.json()['token'] - except: - raise Exception("Failed to get token") - - -if __name__ == "__main__": - def listener(message): - print("\nSubject: " + message['subject']) - print("Content: " + message['text'] if message['text'] else message['html']) - - # Get Domains - test = Email() - print("\nDomain: " + test.domain) - - # Make new email address - test.register() - print("\nEmail Adress: " + str(test.address)) - - # Start listening - test.start(listener) - print("\nWaiting for new emails...") - - # Stop listening - # test.stop() + + async with self.session.post(url, json=payload) as response: + token = (await response.json())["token"] + + self.session.headers.update({"Authorization": f"Bearer {token}"}) diff --git a/src/mailtm/message.py b/src/mailtm/message.py index f22499a..8b0199b 100644 --- a/src/mailtm/message.py +++ b/src/mailtm/message.py @@ -1,51 +1,47 @@ -import json -import time -from threading import Thread +"""Manage messages module.""" + +import asyncio + class Listen: - listen = False - message_ids = [] - - def message_list(self): - url = "https://api.mail.tm/messages" - headers = { 'Authorization': 'Bearer ' + self.token } - response = self.session.get(url, headers=headers) - response.raise_for_status() - - data = response.json() - return [ - msg for i, msg in enumerate(data['hydra:member']) - if data['hydra:member'][i]['id'] not in self.message_ids - ] - - def message(self, idx): - url = "https://api.mail.tm/messages/" + idx - headers = { 'Authorization': 'Bearer ' + self.token } - response = self.session.get(url, headers=headers) - response.raise_for_status() - return response.json() - - def run(self): - while self.listen: - for message in self.message_list(): - self.message_ids.append(message['id']) - message = self.message(message['id']) - self.listener(message) - - time.sleep(self.interval) - - def start(self, listener, interval=3): - if self.listen: - self.stop() - - self.listener = listener - self.interval = interval - self.listen = True - - # Start listening thread - self.thread = Thread(target=self.run) - self.thread.start() - - def stop(self): - self.listen = False - self.thread.join() \ No newline at end of file + """Listener class.""" + + def __init__(self) -> None: + """Init class.""" + self.message_ids = [] + + async def message_list(self) -> list: + """Get a list of messages.""" + url = "messages" + + async with self.session.get(url) as response: + data = await response.json() + + return [ + msg + for i, msg in enumerate(data["hydra:member"]) + if data["hydra:member"][i]["id"] not in self.message_ids + ] + + async def message(self, idx: str) -> dict: + """Get the text of the message.""" + url = f"messages/{idx}" + async with self.session.get(url) as response: + return await response.json() + + async def run(self, listener: any) -> None: + """Run a message check.""" + for message in await self.message_list(): + self.message_ids.append(message["id"]) + listener(await self.message(message["id"])) + + async def start(self, listener: any, interval: int = 3) -> None: + """Run a message check cycle.""" + while True: + try: + await self.run(listener) + await asyncio.sleep(interval) + except KeyboardInterrupt: + print("Stop working") # noqa: T201 + await self.session.close() + break