chore: initial commit
This commit is contained in:
91
src/vmail_client.py
Normal file
91
src/vmail_client.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import re
|
||||
import secrets
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
MAILTM_BASE = "https://api.mail.tm"
|
||||
|
||||
|
||||
class MailClient:
|
||||
"""mail.tm temporary email client."""
|
||||
|
||||
def create_mailbox(self) -> dict:
|
||||
"""Create a mailbox. Returns dict with address, password, token."""
|
||||
domains_resp = httpx.get(f"{MAILTM_BASE}/domains", timeout=30)
|
||||
domains_resp.raise_for_status()
|
||||
domains = domains_resp.json().get("hydra:member", [])
|
||||
active = next((d for d in domains if d.get("isActive")), None)
|
||||
if not active:
|
||||
raise RuntimeError("mail.tm returned no active domains")
|
||||
|
||||
for _ in range(10):
|
||||
address = f"{secrets.token_hex(6)}@{active['domain']}"
|
||||
password = secrets.token_urlsafe(18)
|
||||
r = httpx.post(
|
||||
f"{MAILTM_BASE}/accounts",
|
||||
json={"address": address, "password": password},
|
||||
timeout=30,
|
||||
)
|
||||
if r.status_code == 201:
|
||||
token_resp = httpx.post(
|
||||
f"{MAILTM_BASE}/token",
|
||||
json={"address": address, "password": password},
|
||||
timeout=30,
|
||||
)
|
||||
token_resp.raise_for_status()
|
||||
return {
|
||||
"id": r.json()["id"],
|
||||
"address": address,
|
||||
"password": password,
|
||||
"token": token_resp.json()["token"],
|
||||
}
|
||||
if r.status_code == 422:
|
||||
time.sleep(0.3)
|
||||
continue
|
||||
r.raise_for_status()
|
||||
|
||||
raise RuntimeError("Failed to create mail.tm mailbox after 10 attempts")
|
||||
|
||||
def wait_for_otp(self, mailbox: dict, timeout: int = 120, poll: float = 5.0) -> str:
|
||||
"""Poll mailbox until a 6-digit OTP arrives."""
|
||||
headers = {"Authorization": f"Bearer {mailbox['token']}"}
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
r = httpx.get(
|
||||
f"{MAILTM_BASE}/messages",
|
||||
headers=headers,
|
||||
timeout=30,
|
||||
)
|
||||
r.raise_for_status()
|
||||
messages = r.json().get("hydra:member", [])
|
||||
except httpx.HTTPError as exc:
|
||||
print(f"mail poll error: {exc.__class__.__name__}")
|
||||
time.sleep(poll)
|
||||
continue
|
||||
|
||||
if messages:
|
||||
try:
|
||||
msg_r = httpx.get(
|
||||
f"{MAILTM_BASE}/messages/{messages[0]['id']}",
|
||||
headers=headers,
|
||||
timeout=30,
|
||||
)
|
||||
msg_r.raise_for_status()
|
||||
msg = msg_r.json()
|
||||
except httpx.HTTPError as exc:
|
||||
print(f"mail fetch error: {exc.__class__.__name__}")
|
||||
time.sleep(poll)
|
||||
continue
|
||||
|
||||
html = msg.get("html") or ""
|
||||
if isinstance(html, list):
|
||||
html = "\n".join(html)
|
||||
body = msg.get("text") or html or ""
|
||||
codes = re.findall(r"\b(\d{6})\b", body)
|
||||
if codes:
|
||||
return codes[0]
|
||||
|
||||
time.sleep(poll)
|
||||
raise TimeoutError(f"No OTP received within {timeout}s")
|
||||
Reference in New Issue
Block a user