Skip to content

Commit

Permalink
New entry point script with more options
Browse files Browse the repository at this point in the history
  • Loading branch information
CarloDePieri committed Aug 20, 2020
1 parent 3849c69 commit d9572a9
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 101 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.venv
*/__pycache__/
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mailtm = {path = "."}
[packages]
requests = "*"
random-username = "*"
pyperclip = "*"

[requires]
python_version = "3.8"
9 changes: 8 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This is a python interface to the web api of [mail.tm](https://mail.tm).

The api is documented [here](https://api.mail.tm/).

## Dependencies

`xclip` or `xsel` for clipboard copying.

## Installation

#### With pip
Expand Down Expand Up @@ -36,7 +40,7 @@ pipenv install --dev

## Usage

You can now import the library `pymailtm` and call the utility with the same name:
The utility can be called with:

```bash
pymailtm
Expand All @@ -56,13 +60,24 @@ If using git + Pipenv, and have [invoke](https://github.com/pyinvoke/invoke) ins
inv run
```

Exit the utility by pressing `Ctrl+c`.
By default the command recover the last used account, copy it to the clipboard and wait for a new message to arrive: when
it does, it's opened in the browser in a quick html view.

Exit the waiting loop by pressing `Ctrl+c`.

Calling the utility with the flag `-l` will print the account credentials, open in the browser the
[mail.tm](https://mail.tm) client and exit.

The flag `-n` can be used to force the creation of a new account.

## Security warnings

This is conceived as an insecure, fast throwaway temp mail account generator.
This is conceived as an **insecure**, fast throwaway temp mail account generator.

Do not use it with sensitive data.
**DO NOT** use it with sensitive data.

Mails that arrive while the utility is running will be saved in clear text files in a temp folder (probably `/tmp/`) so
Mails that arrive while the utility is running will be saved in **plain text** files in the system temporary folder (probably `/tmp/`) so
that they can be opened by the browser.

The last used account's data and credentials will be saved in
**plain text** in `~/.pymailtm`.
12 changes: 1 addition & 11 deletions pymailtm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
import signal
import sys
from pymailtm.pymailtm import MailTm, Account, Message


def init():
def signal_handler(sig, frame):
print('\n\nClosing! Bye!')
sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

MailTm().monitor_new_account()
__version__ = '1.0'
215 changes: 145 additions & 70 deletions pymailtm/pymailtm.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,19 @@
import json
import os
import pyperclip
import random
import requests
import string
import webbrowser

from random_username.generate import generate_username
from dataclasses import dataclass
from pathlib import Path
from tempfile import NamedTemporaryFile
from time import sleep
from typing import Dict


class MailTm:
"""A python wrapper for mail.tm web api, which is documented here:
https://api.mail.tm/"""

api_address = "https://api.mail.tm"

def _get_domains_list(self):
r = requests.get("{}/domains".format(self.api_address))
response = r.json()
domains = list(map(lambda x: x["domain"], response["hydra:member"]))
return domains

def get_account(self, password=None):
"""Create and return a new account."""
username = (generate_username(1)[0]).lower()
domain = random.choice(self._get_domains_list())
address = "{}@{}".format(username, domain)
if not password:
password = self._generate_password(6)
response = self._make_account_request("accounts", address, password)
return Account(response["id"], response["address"], password)

def _generate_password(self, length):
letters = string.ascii_letters + string.digits
return ''.join(random.choice(letters) for i in range(length))

@staticmethod
def _make_account_request(endpoint, address, password):
account = {"address": address, "password": password}
headers = {
"accept": "application/ld+json",
"Content-Type": "application/json"
}
r = requests.post("{}/{}".format(MailTm.api_address, endpoint),
data=json.dumps(account), headers=headers)
return r.json()

def monitor_new_account(self):
"""Create a new account and monitor it for new messages."""
account = self.get_account()
print("New account created: {}".format(account.address))
account.monitor_account()


class Account:
"""Representing a temprary mailbox."""

Expand All @@ -67,10 +25,10 @@ def __init__(self, id, address, password):
jwt = MailTm._make_account_request("authentication_token",
self.address, self.password)
self.auth_headers = {
"accept": "application/ld+json",
"Content-Type": "application/json",
"Authorization": "Bearer {}".format(jwt["token"])
}
"accept": "application/ld+json",
"Content-Type": "application/json",
"Authorization": "Bearer {}".format(jwt["token"])
}
self.api_address = MailTm.api_address

def get_messages(self, page=1):
Expand All @@ -79,28 +37,31 @@ def get_messages(self, page=1):
headers=self.auth_headers)
messages = []
for message_data in r.json()["hydra:member"]:
message = Message(
# recover full message
r = requests.get(
f"{self.api_address}/messages/{message_data['id']}", headers=self.auth_headers)
text = r.json()["text"]
html = r.json()["html"]
# prepare the mssage object
messages.append(Message(
message_data["id"],
message_data["from"],
message_data["to"],
message_data["subject"],
message_data["intro"],
message_data,
)
r = requests.get(f"{self.api_address}/messages/{message.id_}", headers=self.auth_headers)
message.text = r.json()["text"]
message.html = r.json()["html"]
messages.append(message)
text,
html,
message_data))
return messages

def delete_account(self):
"""Try to delete the account. Returns True if it succeeds."""
r = requests.delete("{}/accounts/{}".format(self.api_address,
self.id_), headers=self.auth_headers)
self.id_), headers=self.auth_headers)
return r.status_code == 204

def monitor_account(self):
"""Keep waiting for new messages and open then in the browser."""
"""Keep waiting for new messages and open them in the browser."""
while True:
print("\nWaiting for new messages...")
start = len(self.get_messages())
Expand All @@ -112,12 +73,14 @@ def monitor_account(self):

@dataclass
class Message:
"""Simple data class that holds a message informations."""
"""Simple data class that holds a message information."""
id_: str
from_: Dict
to: Dict
subject: str
intro: str
text: str
html: str
data: Dict

def open_web(self):
Expand All @@ -136,18 +99,130 @@ def open_web(self):

f.write(message)
f.flush()
os.fsync(f)
file_name = f.name

# webbrowser must be silent!
saverr = os.dup(2)
os.close(2)
os.open(os.devnull, os.O_RDWR)
open_webbrowser("file://{}".format(file_name))
# Wait a second before deleting the tempfile, so that the
# browser can load it safely
sleep(1)
os.remove(file_name)


def open_webbrowser(link: str) -> None:
"""Open a url in the browser ignoring error messages."""
saverr = os.dup(2)
os.close(2)
os.open(os.devnull, os.O_RDWR)
try:
webbrowser.open(link)
finally:
os.dup2(saverr, 2)


class CouldNotGetAccountException(Exception):
"""Raised if a POST on /accounts or /authorization_token return a failed status code."""


class InvalidDbAccountException(Exception):
"""Raised if an account could not be recovered from the db file."""


class MailTm:
"""A python wrapper for mail.tm web api, which is documented here:
https://api.mail.tm/"""

api_address = "https://api.mail.tm"
db_file = os.path.join(Path.home(), ".pymailtm")

def _get_domains_list(self):
r = requests.get("{}/domains".format(self.api_address))
response = r.json()
domains = list(map(lambda x: x["domain"], response["hydra:member"]))
return domains

def get_account(self, password=None):
"""Create and return a new account."""
username = (generate_username(1)[0]).lower()
domain = random.choice(self._get_domains_list())
address = "{}@{}".format(username, domain)
if not password:
password = self._generate_password(6)
response = self._make_account_request("accounts", address, password)
account = Account(response["id"], response["address"], password)
self._save_account(account)
return account

def _generate_password(self, length):
letters = string.ascii_letters + string.digits
return ''.join(random.choice(letters) for i in range(length))

@staticmethod
def _make_account_request(endpoint, address, password):
account = {"address": address, "password": password}
headers = {
"accept": "application/ld+json",
"Content-Type": "application/json"
}
r = requests.post("{}/{}".format(MailTm.api_address, endpoint),
data=json.dumps(account), headers=headers)
if r.status_code not in [200, 201]:
raise CouldNotGetAccountException()
return r.json()

def monitor_new_account(self, force_new=False):
"""Create a new account and monitor it for new messages."""
account = self._open_account(new=force_new)
account.monitor_account()

def _save_account(self, account: Account):
"""Save the account data for later use."""
data = {
"id": account.id_,
"address": account.address,
"password": account.password
}
with open(self.db_file, "w+") as db:
json.dump(data, db)

def _load_account(self):
"""Return the last used account."""
with open(self.db_file, "r") as db:
data = json.load(db)
# send a /me request to ensure the account is there
if "address" not in data or "password" not in data or "id" not in data:
# No valid db file was found, raise
raise InvalidDbAccountException()
else:
return Account(data["id"], data["address"], data["password"])

def _open_account(self, new=False):
"""Recover a saved account data, check if it's still there and return that one; otherwise create a new one and
return it.
:param new: bool - force the creation of a new account"""
def _new():
account = self.get_account()
print("New account created and copied to clipboard: {}".format(
account.address))
return account
if new:
account = _new()
else:
try:
webbrowser.open("file://{}".format(file_name))
finally:
os.dup2(saverr, 2)
# Wait a second before deleting the tempfile, so that the
# browser can load it safely
sleep(1)
os.remove(file_name)
account = self._load_account()
print("Account recovered and copied to clipboard: {}".format(
account.address))
except Exception:
account = _new()
pyperclip.copy(account.address)
print("")
return account

def browser_login(self, new=False):
"""Print login credentials and open the login page in the browser."""
account = self._open_account(new=new)
print("\nAccount credentials:")
print("\nEmail: {}".format(account.address))
print("Password: {}\n".format(account.password))
open_webbrowser("https://mail.tm/")
sleep(1) # Allow for the output of webbrowser to arrive
5 changes: 0 additions & 5 deletions run.py

This file was deleted.

Loading

0 comments on commit d9572a9

Please sign in to comment.