"""BAN YARO — Mailing (Admin)""" import imaplib import os import smtplib import ssl from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.utils import formataddr from datetime import datetime from typing import List, Optional import logging from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from auth import require_admin from database import db router = APIRouter() _log = logging.getLogger(__name__) _SMTP_HOST = os.getenv("SMTP_HOST", "mail.your-server.de") _SMTP_PORT = int(os.getenv("SMTP_PORT", "587")) _IMAP_HOST = os.getenv("IMAP_HOST", "mail.your-server.de") _IMAP_PORT = int(os.getenv("IMAP_PORT", "993")) _ACCOUNTS = { "partner": { "user": os.getenv("SMTP_USER", ""), "pass": os.getenv("SMTP_PASS", ""), "from": "partner@banyaro.app", "name": "Ban Yaro Partner", }, "support": { "user": os.getenv("SMTP_SUPPORT_USER", "support@banyaro.de"), "pass": os.getenv("SMTP_SUPPORT_PASS", ""), "from": "support@banyaro.app", "name": "Ban Yaro Support", }, } # Mögliche Namen für den Sent-Ordner (Hetzner/Dovecot) _SENT_CANDIDATES = ["Sent", "Sent Messages", "Sent Items", "INBOX.Sent", "Gesendete Objekte"] def _imap_save_sent(msg_bytes: bytes, account: str): acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"] if not acc["user"] or not acc["pass"]: _log.warning("IMAP: Account '%s' nicht konfiguriert, überspringe.", account) return try: ctx = ssl.create_default_context() with imaplib.IMAP4_SSL(_IMAP_HOST, _IMAP_PORT, ssl_context=ctx) as imap: imap.login(acc["user"], acc["pass"]) _, raw_folders = imap.list() available = [f.decode(errors="replace") for f in (raw_folders or [])] _log.info("IMAP Ordner (%s): %s", account, available) # Echten Ordnernamen aus LIST-Antwort extrahieren # Format: '(\Flags) "." INBOX.Sent' → letztes Token folder = None for line in available: name = line.rsplit('"." ', 1)[-1].strip().strip('"') for candidate in _SENT_CANDIDATES: if candidate.lower() in name.lower(): folder = name break if folder: break if not folder: folder = "INBOX.Sent" _log.info("IMAP: speichere in Ordner '%s' (%s)", folder, account) typ, data = imap.append( folder, r"\Seen", imaplib.Time2Internaldate(datetime.now().timestamp()), msg_bytes, ) _log.info("IMAP append: %s %s", typ, data) except Exception as e: _log.error("IMAP Sent-Speicherung fehlgeschlagen (%s): %s", account, e) def _build_message(to: str, subject: str, body: str, account: str) -> MIMEMultipart: acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"] msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = formataddr((acc["name"], acc["from"])) msg["To"] = to msg["Reply-To"] = acc["from"] msg.attach(MIMEText(body, "plain", "utf-8")) return msg def _send_smtp(to: str, subject: str, body: str, account: str = "partner"): acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"] if not acc["user"] or not acc["pass"]: raise RuntimeError(f"SMTP-Account '{account}' nicht konfiguriert.") msg = _build_message(to, subject, body, account) msg_bytes = msg.as_bytes() ctx = ssl.create_default_context() with smtplib.SMTP(_SMTP_HOST, _SMTP_PORT, timeout=15) as s: s.ehlo() s.starttls(context=ctx) s.login(acc["user"], acc["pass"]) s.sendmail(acc["from"], [to], msg_bytes) _imap_save_sent(msg_bytes, account) # ------------------------------------------------------------------ # Schemas # ------------------------------------------------------------------ class TemplateIn(BaseModel): key: str label: str subject: str body: str from_account: str = "partner" class TemplateUpdate(BaseModel): label: str subject: str body: str from_account: str = "partner" class SendRequest(BaseModel): to: List[str] subject: str body: str from_account: str = "partner" template_id: Optional[int] = None # ------------------------------------------------------------------ # Templates CRUD # ------------------------------------------------------------------ @router.get("/templates") def list_templates(user=Depends(require_admin)): with db() as conn: rows = conn.execute( "SELECT id, key, label, subject, body, from_account FROM email_templates ORDER BY id" ).fetchall() return [dict(r) for r in rows] @router.post("/templates", status_code=201) def create_template(data: TemplateIn, user=Depends(require_admin)): try: with db() as conn: row = conn.execute( """INSERT INTO email_templates (key, label, subject, body, from_account, created_at) VALUES (?, ?, ?, ?, ?, ?) RETURNING id""", (data.key, data.label, data.subject, data.body, data.from_account, datetime.utcnow().isoformat()) ).fetchone() return {"id": row["id"]} except Exception as e: raise HTTPException(400, f"Vorlage konnte nicht angelegt werden: {e}") @router.put("/templates/{tpl_id}") def update_template(tpl_id: int, data: TemplateUpdate, user=Depends(require_admin)): with db() as conn: conn.execute( """UPDATE email_templates SET label=?, subject=?, body=?, from_account=?, updated_at=? WHERE id=?""", (data.label, data.subject, data.body, data.from_account, datetime.utcnow().isoformat(), tpl_id) ) return {"ok": True} @router.delete("/templates/{tpl_id}") def delete_template(tpl_id: int, user=Depends(require_admin)): with db() as conn: conn.execute("DELETE FROM email_templates WHERE id=?", (tpl_id,)) return {"ok": True} # ------------------------------------------------------------------ # Senden # ------------------------------------------------------------------ @router.post("/send") def send_mail(data: SendRequest, user=Depends(require_admin)): if not data.to: raise HTTPException(400, "Mindestens eine Empfänger-Adresse angeben.") if not data.subject.strip() or not data.body.strip(): raise HTTPException(400, "Betreff und Text dürfen nicht leer sein.") sent, failed = [], [] for addr in data.to: addr = addr.strip() if not addr: continue try: _send_smtp(addr, data.subject, data.body, data.from_account) sent.append(addr) with db() as conn: conn.execute( """INSERT INTO outreach_log (sent_by, recipient, subject, body, from_account, sent_at) VALUES (?, ?, ?, ?, ?, ?)""", (user["id"], addr, data.subject, data.body, data.from_account, datetime.utcnow().isoformat()) ) except Exception as e: failed.append({"addr": addr, "error": str(e)}) return {"sent": sent, "failed": failed} # ------------------------------------------------------------------ # Support-Versand (intern, ohne Admin-Auth — für Moderatoren-Trigger) # ------------------------------------------------------------------ def send_support_mail(to: str, subject: str, body: str): """Intern aufrufbar ohne HTTP-Request — z.B. aus Moderations-Logik.""" _send_smtp(to, subject, body, "support") # ------------------------------------------------------------------ # Log # ------------------------------------------------------------------ @router.get("/log") def outreach_log_endpoint(user=Depends(require_admin)): with db() as conn: rows = conn.execute( """SELECT ol.id, ol.recipient, ol.subject, ol.sent_at, ol.from_account, u.name AS sent_by_name FROM outreach_log ol JOIN users u ON u.id = ol.sent_by ORDER BY ol.sent_at DESC LIMIT 200""" ).fetchall() return [dict(r) for r in rows]