rmi-backend/app/email_router.py

161 lines
5.8 KiB
Python

"""
Email Service — Listmonk + Gmail SMTP integration for rugmunch.io
Uses listmonk API for newsletters + transactional emails.
SMTP relay: Gmail (cryptorugmuncher@gmail.com)
"""
import logging
import os
from datetime import UTC, datetime
import httpx
from fastapi import APIRouter, HTTPException
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/email", tags=["email"])
LISTMONK_URL = "http://127.0.0.1:9001/api"
LISTMONK_USER = "admin"
LISTMONK_PASS = "rugmunch2024"
_token_cache = {"token": None, "expires": 0}
async def _get_token() -> str:
"""Get listmonk API token, cached."""
import time
if _token_cache["token"] and time.time() < _token_cache["expires"]:
return _token_cache["token"]
async with httpx.AsyncClient(timeout=10) as c:
r = await c.post(
f"{LISTMONK_URL}/auth/login",
json={"username": LISTMONK_USER, "password": LISTMONK_PASS},
)
if r.status_code == 200:
data = r.json().get("data", {})
token = data.get("token", "")
_token_cache["token"] = token
_token_cache["expires"] = time.time() + 3600
return token
raise HTTPException(status_code=500, detail="Listmonk auth failed")
# ═══════════════════════════════════════════════════════════
# SUBSCRIBERS
# ═══════════════════════════════════════════════════════════
@router.get("/subscribers")
async def list_subscribers():
token = await _get_token()
async with httpx.AsyncClient(timeout=10) as c:
r = await c.get(f"{LISTMONK_URL}/subscribers", headers={"Authorization": f"Bearer {token}"})
return r.json()
@router.post("/subscribers")
async def add_subscriber(data: dict):
email = data.get("email", "")
name = data.get("name", "")
if not email:
raise HTTPException(status_code=400, detail="email required")
token = await _get_token()
async with httpx.AsyncClient(timeout=10) as c:
r = await c.post(
f"{LISTMONK_URL}/subscribers",
headers={"Authorization": f"Bearer {token}"},
json={"email": email, "name": name, "status": "enabled"},
)
return r.json()
# ═══════════════════════════════════════════════════════════
# NEWSLETTER CAMPAIGNS
# ═══════════════════════════════════════════════════════════
@router.post("/campaign")
async def create_campaign(data: dict):
subject = data.get("subject", "RMI Intelligence Update")
body = data.get("body", "")
list_ids = data.get("list_ids", [1])
if not body:
raise HTTPException(status_code=400, detail="body required")
token = await _get_token()
async with httpx.AsyncClient(timeout=10) as c:
r = await c.post(
f"{LISTMONK_URL}/campaigns",
headers={"Authorization": f"Bearer {token}"},
json={
"name": subject,
"subject": subject,
"lists": list_ids,
"content_type": "html",
"body": body,
"send_at": None,
},
)
return r.json()
@router.get("/campaigns")
async def list_campaigns():
token = await _get_token()
async with httpx.AsyncClient(timeout=10) as c:
r = await c.get(f"{LISTMONK_URL}/campaigns", headers={"Authorization": f"Bearer {token}"})
return r.json()
# ═══════════════════════════════════════════════════════════
# TRANSACTIONAL — Direct Gmail SMTP
# ═══════════════════════════════════════════════════════════
@router.post("/send")
async def send_transactional(data: dict):
"""Send a single transactional email via Gmail SMTP."""
to = data.get("to", "")
subject = data.get("subject", "RMI Alert")
body = data.get("body", "")
if not to or not body:
raise HTTPException(status_code=400, detail="to and body required")
try:
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
msg = MIMEMultipart()
msg["From"] = "Rug Munch Intelligence <cryptorugmuncher@gmail.com>"
msg["To"] = to
msg["Subject"] = subject
msg.attach(MIMEText(body, "html"))
with smtplib.SMTP("smtp.gmail.com", 587, timeout=15) as server:
server.starttls()
# Gmail App Password needed — stored in env
app_password = os.getenv("GMAIL_APP_PASSWORD", "")
server.login("cryptorugmuncher@gmail.com", app_password)
server.send_message(msg)
return {
"status": "sent",
"to": to,
"subject": subject,
"timestamp": datetime.now(UTC).isoformat(),
}
except Exception as e:
return {
"status": "failed",
"error": str(e),
"hint": "Gmail App Password required. Set GMAIL_APP_PASSWORD env var",
}
@router.get("/status")
async def email_status():
return {
"provider": "listmonk + gmail",
"smtp": "smtp.gmail.com:587",
"sender": "cryptorugmuncher@gmail.com",
"domain": "rugmunch.io",
"listmonk_url": "http://localhost:9001",
"newsletter_enabled": True,
"transactional_enabled": True,
}