Feature: Admin Outreach — E-Mail-Versand via Hetzner SMTP, Vorlagen, Log, SW by-v567

This commit is contained in:
rene 2026-04-30 19:10:54 +02:00
parent 230455c250
commit b6258db6bc
6 changed files with 261 additions and 2 deletions

123
backend/routes/outreach.py Normal file
View file

@ -0,0 +1,123 @@
"""BAN YARO — Outreach E-Mail (Admin)"""
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 fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from auth import require_admin
from database import db
router = APIRouter()
_SMTP_HOST = os.getenv("SMTP_HOST", "")
_SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
_SMTP_USER = os.getenv("SMTP_USER", "")
_SMTP_PASS = os.getenv("SMTP_PASS", "")
_SMTP_FROM = os.getenv("SMTP_FROM", "partner@banyaro.app")
TEMPLATES = {
"influencer_de": {
"label": "Influencer-Ansprache (DE)",
"subject": "Ban Yaro — 100 Gründer-Plätze, einer davon für deine Community",
"body": """Hallo {name},
ich bin René und habe Ban Yaro gebaut eine Hunde-App für Tagebuch, Gesundheit, Giftköder-Alarm und Community. Kostenlos, ohne App Store, direkt als PWA.
Ich kontaktiere gerade einige Influencer aus der deutschen Hunde-Community mit einem konkreten Angebot:
Was deine Follower bekommen: Wer sich mit deinem persönlichen Code registriert, sichert sich einen der 100 Gründer-Plätze eine nummerierte Badge ("Gründer #42") die dauerhaft im Profil sichtbar ist. Diese 100 Plätze gibt es genau einmal.
Was du bekommst: Partner-Badge in der App, eigener Code, öffentliches Ranking wer die meisten Gründer bringt.
Kein Geld, kein verpflichtender Post aber eine echte Exklusivität die du deiner Community geben kannst.
Alle Infos: https://banyaro.app/partner
Wenn dich das interessiert, antworte einfach kurz ich richte deinen Code binnen 24h ein.
Viele Grüße,
René
banyaro.app""",
},
}
class SendRequest(BaseModel):
to: List[str]
subject: str
body: str
template_name: Optional[str] = None
def _send_smtp(to: str, subject: str, body: str):
if not _SMTP_HOST or not _SMTP_USER:
raise RuntimeError("SMTP nicht konfiguriert.")
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = formataddr(("Ban Yaro Partner", _SMTP_FROM))
msg["To"] = to
msg["Reply-To"] = _SMTP_FROM
msg.attach(MIMEText(body, "plain", "utf-8"))
ctx = ssl.create_default_context()
with smtplib.SMTP(_SMTP_HOST, _SMTP_PORT, timeout=15) as s:
s.ehlo()
s.starttls(context=ctx)
s.login(_SMTP_USER, _SMTP_PASS)
s.sendmail(_SMTP_FROM, to, msg.as_bytes())
@router.get("/templates")
def list_templates(user=Depends(require_admin)):
return [{"id": k, "label": v["label"], "subject": v["subject"], "body": v["body"]}
for k, v in TEMPLATES.items()]
@router.post("/send")
def send_outreach(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)
sent.append(addr)
# Log in DB
with db() as conn:
conn.execute(
"""INSERT INTO outreach_log
(sent_by, recipient, subject, body, sent_at)
VALUES (?, ?, ?, ?, ?)""",
(user["id"], addr, data.subject, data.body,
datetime.utcnow().isoformat())
)
except Exception as e:
failed.append({"addr": addr, "error": str(e)})
return {"sent": sent, "failed": failed}
@router.get("/log")
def outreach_log(user=Depends(require_admin)):
with db() as conn:
rows = conn.execute(
"""SELECT ol.id, ol.recipient, ol.subject, ol.sent_at,
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 100"""
).fetchall()
return [dict(r) for r in rows]