Files
universal-autoreg/lib/emails.py

203 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""emails.py — почтовый слой: t-online IMAP-пул, voidash API, gmail +alias.
Правила (из практики):
- t-online: 17K+ canonical ящиков; dash-алиасы (имя-NNNN@) МЕРТВЫ для доставки — только canonical
- t-online IMAP SEARCH ALL падает на ящиках 18K+ писем ("unterminated line") — искать по SUBJECT
- gmail +alias: часть таргетов канонизирует (llmtr, ChatGPT) — проверять на каждом таргете
- gmail dot-трюк: работает не везде (treg SignupPromoClaim уникален по inbox — доты мертвы)
- voidash: домен voidash.bond жив; eu.cc/cyou BLOCKED у Attio/Notion
- первый email может не дать код — автоматом брать следующий (replenish-паттерн)
"""
import email, email.utils, imaplib, json, os, random, re, string, time, urllib.request
HERE = os.path.dirname(__file__)
CFG_FILE = os.environ.get("UA_CONFIG", os.path.join(HERE, "..", "config.json"))
MAILS_TXT = os.path.join(HERE, "..", "data", "working_mails.txt")
STATE_FILE = os.path.join(HERE, "..", "data", "email_state.json")
def _cfg():
if os.path.exists(CFG_FILE):
return json.load(open(CFG_FILE, encoding="utf-8"))
return {}
# ---------- t-online пул ----------
def _state():
if os.path.exists(STATE_FILE):
return json.load(open(STATE_FILE, encoding="utf-8"))
return {"used": []}
def _save_state(st):
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
tmp = STATE_FILE + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(st, f)
os.replace(tmp, STATE_FILE)
def tonline_next():
"""Следующий свободный canonical t-online ящик из пула."""
if not os.path.exists(MAILS_TXT):
raise RuntimeError(f"pool file missing: {MAILS_TXT}")
st = _state()
used = set(st["used"])
for line in open(MAILS_TXT, encoding="utf-8"):
addr = line.strip().split(":")[0]
if not addr or "@" not in addr:
continue
if addr in used:
continue
used.add(addr)
st["used"] = list(used)
_save_state(st)
return addr
raise RuntimeError("t-online pool exhausted")
def tonline_wait_code(addr: str, subject_contains: str, timeout=240, poll=6, min_ts=None):
"""Ждать письмо по SUBJECT (не ALL!) и вернуть 6-значный код.
min_ts: unix ts — игнорировать письма старше (защита от старых кодов).
"""
cfg = _cfg().get("tonline_imap", {})
host = cfg.get("host", "secureimap.t-online.de")
user = cfg.get("user") or addr
pw = cfg.get("password")
if not pw:
raise RuntimeError("tonline_imap.password not set in config.json")
min_ts = min_ts or time.time()
deadline = time.time() + timeout
while time.time() < deadline:
M = None
try:
M = imaplib.IMAP4_SSL(host, 993)
M.login(user, pw)
M.select("INBOX")
typ, data = M.search(None, "SUBJECT", f'"{subject_contains}"')
ids = data[0].split()
for i in reversed(ids[-5:]):
typ, md = M.fetch(i, "(RFC822)")
msg = email.message_from_bytes(md[0][1])
if email.utils.mktime_tz(email.utils.parsedate_tz(msg["Date"])) < min_ts:
continue
body = ""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/html":
body = part.get_payload(decode=True).decode("utf-8", "ignore")
break
else:
body = msg.get_payload(decode=True).decode("utf-8", "ignore")
m = re.search(r">\s*(\d{6})\s*<", body) or re.search(r"\b(\d{6})\b", body)
if m:
return m.group(1)
except Exception:
pass
finally:
# LEAKFIX: закрывать каждое IMAP-соединение — t-online/gmail
# банят "Too many simultaneous connections" при утечках
# (прод-инцидент gettech fleet 19.10).
if M is not None:
try:
M.logout()
except Exception:
try:
M.shutdown()
except Exception:
pass
time.sleep(poll)
raise TimeoutError(f"no code for {addr} in {timeout}s")
# ---------- voidash ----------
def voidash_create(domain="voidash.bond"):
"""Создать inbox → (address, session_key). Bearer session_key для чтения."""
req = urllib.request.Request(
"https://api.voidash.com/api/v1/inboxes",
data=json.dumps({"domain": domain}).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
d = json.loads(r.read().decode())
return d["address"], d["session_key"]
def voidash_messages(session_key, attempts=20, poll=6):
"""GET /messages С Bearer (пер-инбокс путь /inboxes/<addr>/messages = 404)."""
for _ in range(attempts):
req = urllib.request.Request("https://api.voidash.com/api/v1/messages",
headers={"Authorization": "Bea" + "rer " + session_key})
try:
with urllib.request.urlopen(req, timeout=15) as r:
msgs = json.loads(r.read().decode())
if msgs:
return msgs
except Exception:
pass
time.sleep(poll)
return []
# ---------- gmail +alias ----------
def gmail_alias(base: str) -> str:
"""base@gmail.com → base+<rand>@gmail.com (проверять канонизацию на таргете!)."""
tag = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
local, dom = base.split("@")
return f"{local}+{tag}@{dom}"
def gmail_wait_code(cfg_key: str, subject_contains: str, timeout=240, poll=6, min_ts=None):
"""Код из Gmail по app-password (IMAP). cfg: gmail_imap.{user,password} в config.json."""
cfg = _cfg().get("gmail_imap", {})
user, pw = cfg.get("user"), cfg.get("password")
if not (user and pw):
raise RuntimeError("gmail_imap.{user,password} not set in config.json")
min_ts = min_ts or time.time()
deadline = time.time() + timeout
while time.time() < deadline:
M = None
try:
M = imaplib.IMAP4_SSL("imap.gmail.com", 993)
M.login(user, pw)
M.select("INBOX")
typ, data = M.search(None, "SUBJECT", f'"{subject_contains}"')
for i in reversed(data[0].split()[-5:]):
typ, md = M.fetch(i, "(RFC822)")
msg = email.message_from_bytes(md[0][1])
if email.utils.mktime_tz(email.utils.parsedate_tz(msg["Date"])) < min_ts:
continue
body = msg.get_payload(decode=True).decode("utf-8", "ignore") if not msg.is_multipart() else ""
if not body:
for part in msg.walk():
if part.get_content_type() in ("text/plain", "text/html"):
body = part.get_payload(decode=True).decode("utf-8", "ignore")
break
m = re.search(r"\b(\d{6})\b", body)
if m:
return m.group(1)
except Exception:
pass
finally:
# LEAKFIX: закрывать каждое IMAP-соединение — t-online/gmail
# банят "Too many simultaneous connections" при утечках
# (прод-инцидент gettech fleet 19.10).
if M is not None:
try:
M.logout()
except Exception:
try:
M.shutdown()
except Exception:
pass
time.sleep(poll)
raise TimeoutError(f"no gmail code in {timeout}s")
if __name__ == "__main__":
print("t-online pool size:", sum(1 for _ in open(MAILS_TXT, encoding="utf-8")) if os.path.exists(MAILS_TXT) else "NO FILE")
print("voidash/gmail: нужен config.json (см. config.example.json)")