rmi-backend/app/_archive/legacy_2026_07/email_router.py
cryptorugmunch 628c1d2a10
Some checks failed
CI / build (push) Failing after 3s
refactor(rmi-backend,audit): mount Wave 3 + archive 136 dead-code files (P2.3)
PHASE 2.3 (AUDIT-2026-Q3.md):

Task 1 — Wire-in Wave 3 (1 router mounted, 2 deferred):
  - app.routers.unified_scanner_router mounted at /api/v2/scanner/* (2 routes:
    POST /api/v2/scanner/token/scan, POST /api/v2/scanner/wallet/scan).
    Refactored prefix from /api/v2 -> /api/v2/scanner to avoid future conflicts
    with the v1 /api/v1/scanner/ stub.
  - app.routers.unified_wallet_scanner DEFERRED (no router APIRouter attribute;
    library module consumed by unified_scanner_router via get_wallet_scanner()).
  - app.routers.admin_extensions DEFERRED (DORMANT per audit; 25 routes at
    /api/v1/admin/* would shadow /api/v1/admin/alerts_webhook).

Task 2 — Archive 136 dead-code files to app/_archive/legacy_2026_07/:
  - 73 routers in app/routers/ (reach graph showed zero reach into mount.py).
  - 63 flat app/*.py (domain modules never imported by live code).
  - 1 file RESTORED post-archive: app/routers/x402_bridge_health.py (caught by
    tests/unit/test_bridge_health.py which directly imports it; reach graph
    considered tests/ only as transitive reach — to be patched in next cycle).

Forced-LIVE (NOT archived per user directive):
  - app/ai_pipeline_v3.py  (3 importers in audit window, importers themselves DEAD)
  - app/splade_bm25.py       (LIVE via app.rag_service)
  - app/wallet_manager_v2.py (LIVE via x402_enforcement, x402_tools, sweep_all, sweep_now)
  - app/crypto_embeddings.py (NOT in audit ARCHIVE list; heavy import graph)

Verification (forward-import closure from mount.py + main.py + factory.py + lifespan.py):
  - imports = 348 app.* modules
  - reached = 194 files reachable from roots
  - archive set = audit_dead (186) - reached - forced_live (4) - test_live (1) = 136
  - Net delta: 136 files moved, 44,932 LOC reduction, 293->295 active routes (+2 from Wave 3)

pyproject.toml updates:
  - setuptools.packages.find: added exclude for app._archive*
  - ruff.extend-exclude: added "app/_archive/"
  - mypy.exclude: added "app/_archive/"

Smoke test: pytest tests/ — 817 passed, 3 pre-existing failures unchanged
(0 new failures; 0 routes lost; all 4 forced-LIVE files still importable).

Restoration: git mv app/_archive/legacy_2026_07/<name>.py <original-path>
and add the import to app/mount.py ROUTER_MODULES.

Refs: AUDIT-2026-Q3.md /home/dev/pry/rmi-final-deadcode-2026-07-06.md
2026-07-06 20:52:31 +02:00

135 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""
Email API Router - Maildir + SMTP access for all RMI email accounts.
Maildir is mounted from host at /var/mail/vhosts.
SMTP uses host gateway for outbound delivery.
"""
import email
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
router = APIRouter(prefix="/email", tags=["email"])
SMTP_HOST = os.getenv("SMTP_HOST", "172.20.0.1")
SMTP_PORT = int(os.getenv("SMTP_PORT", "25"))
MAILDIR_BASE = "/var/mail/vhosts"
ACCOUNTS = {
"admin": "admin@rugmunch.io",
"biz": "biz@rugmunch.io",
"security": "security@rugmunch.io",
"hello": "hello@cryptorugmunch.com",
"media": "media@cryptorugmunch.com",
}
class SendRequest(BaseModel):
sender_account: str
to: str
subject: str
body: str
def _maildir_for(account: str) -> str:
addr = ACCOUNTS.get(account)
if not addr:
raise HTTPException(404, f"Unknown account: {account}")
local, domain = addr.split("@")
return f"{MAILDIR_BASE}/{domain}/{local}"
def _read_msg(filepath: str, msg_id: int, full: bool = False) -> dict:
with open(filepath, "rb") as f:
msg = email.message_from_binary_file(f)
result = {
"id": msg_id,
"subject": str(msg.get("Subject", "")),
"sender": str(msg.get("From", "")),
"recipient": str(msg.get("To", "")),
"date": str(msg.get("Date", "")),
}
if full:
body = ""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
payload = part.get_payload(decode=True)
if payload:
body = payload.decode(errors="replace")
break
else:
payload = msg.get_payload(decode=True)
if payload:
body = payload.decode(errors="replace")
result["body"] = body[:10000]
return result
def _list_files(maildir: str) -> list:
files = []
for sub in ["new", "cur"]:
d = os.path.join(maildir, sub)
if os.path.isdir(d):
files.extend(os.path.join(d, f) for f in os.listdir(d))
files.sort(key=lambda p: os.path.getmtime(p), reverse=True)
return files
@router.get("/accounts")
def list_accounts():
return {"accounts": dict(ACCOUNTS.items())}
@router.get("/{account}/inbox")
def list_inbox(account: str, limit: int = Query(20, le=100)):
if account not in ACCOUNTS:
raise HTTPException(404, f"Unknown account. Options: {list(ACCOUNTS.keys())}")
try:
maildir = _maildir_for(account)
files = _list_files(maildir)[:limit]
msgs = [_read_msg(f, i + 1) for i, f in enumerate(files)]
return {"account": ACCOUNTS[account], "count": len(msgs), "messages": msgs}
except HTTPException:
raise
except Exception as e:
raise HTTPException(500, str(e)) from e
@router.get("/{account}/message/{msg_id}")
def read_message(account: str, msg_id: int):
if account not in ACCOUNTS:
raise HTTPException(404, "Unknown account")
try:
maildir = _maildir_for(account)
files = _list_files(maildir)
if msg_id < 1 or msg_id > len(files):
raise HTTPException(404, "Message not found")
return _read_msg(files[msg_id - 1], msg_id, full=True)
except HTTPException:
raise
except Exception as e:
raise HTTPException(500, str(e)) from e
@router.post("/send")
def send_email(req: SendRequest):
if req.sender_account not in ACCOUNTS:
raise HTTPException(404, "Unknown sender account")
sender_addr = ACCOUNTS[req.sender_account]
msg = MIMEMultipart()
msg["From"] = sender_addr
msg["To"] = req.to
msg["Subject"] = req.subject
msg.attach(MIMEText(req.body, "plain"))
try:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as smtp:
smtp.send_message(msg)
return {"status": "sent", "from": sender_addr, "to": req.to}
except Exception as e:
raise HTTPException(500, f"Send failed: {e}") from e