import logging import smtplib from datetime import datetime from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr from typing import Sequence from sqlalchemy.orm import Session from app.core.config import settings from app.core.email_utils import parse_recipient_list from app.models.system import SendLog from app.services.config_store import get_config logger = logging.getLogger("newsletter.smtp") def get_smtp_settings(db: Session) -> dict[str, str]: return { "enabled": get_config(db, "smtp.enabled", "false"), "host": get_config(db, "smtp.host", ""), "port": get_config(db, "smtp.port", "587"), "username": get_config(db, "smtp.username", ""), "password": get_config(db, "smtp.password", ""), "from_email": get_config(db, "smtp.from_email", ""), "from_name": get_config(db, "smtp.from_name", ""), "reply_to": get_config(db, "smtp.reply_to", ""), "use_tls": get_config(db, "smtp.use_tls", "true"), "recipients": get_config(db, "smtp.recipients", ""), # Zeitplan "schedule_enabled": get_config(db, "smtp.schedule_enabled", "false"), "schedule_acknowledged": get_config(db, "smtp.schedule_acknowledged", "false"), "schedule_frequency": get_config(db, "smtp.schedule_frequency", "monthly"), "schedule_time": get_config(db, "smtp.schedule_time", "08:00"), "schedule_weekdays": get_config(db, "smtp.schedule_weekdays", "0"), "schedule_dom": get_config(db, "smtp.schedule_dom", "1"), "last_scheduled_date": get_config(db, "smtp.last_scheduled_date", ""), # Generierungs-Vorgaben für den geplanten Versand "gen_period": get_config(db, "smtp.gen_period", "last_month"), "gen_days": get_config(db, "smtp.gen_days", "30"), "gen_article_type": get_config(db, "smtp.gen_article_type", "new"), "gen_category": get_config(db, "smtp.gen_category", ""), "gen_excluded_users": get_config(db, "smtp.gen_excluded_users", settings.default_excluded_users), } def schedule_is_due(settings: dict[str, str], now: datetime) -> bool: """Prüft, ob laut Zeitplan jetzt ein Versand fällig ist (mit Tages-Dedupe).""" if settings["enabled"].lower() != "true": return False if (settings.get("schedule_enabled") or "false").lower() != "true": return False if (settings.get("schedule_acknowledged") or "false").lower() != "true": return False freq = (settings.get("schedule_frequency") or "off").lower() if freq == "off": return False schedule_time = settings.get("schedule_time") or "08:00" if now.strftime("%H:%M") < schedule_time: return False today = now.strftime("%Y-%m-%d") if settings.get("last_scheduled_date") == today: return False if freq == "daily": return True if freq == "weekly": weekdays = {d.strip() for d in (settings.get("schedule_weekdays") or "").split(",") if d.strip() != ""} return str(now.weekday()) in weekdays if freq == "monthly": try: dom = int(settings.get("schedule_dom") or "1") except ValueError: dom = 1 return now.day == max(1, min(dom, 28)) return False def send_newsletter( db: Session, subject: str, html_content: str, text_content: str, recipients: Sequence[str], scheduled: bool = False, ) -> tuple[str, str]: settings = get_smtp_settings(db) if settings["enabled"].lower() != "true": detail = "SMTP ist deaktiviert. Bitte in der SMTP-Konfiguration aktivieren." _log(db, subject, recipients, "skipped", detail, scheduled) return "skipped", detail if not settings["host"] or not settings["from_email"] or not recipients: detail = "SMTP unvollständig konfiguriert (Host, Absender oder Empfänger fehlen)." _log(db, subject, recipients, "failed", detail, scheduled) return "failed", detail try: validated_recipients = parse_recipient_list(",".join(recipients)) except ValueError as exc: detail = f"Ungültige Empfängerliste: {exc}" _log(db, subject, recipients, "failed", detail, scheduled) return "failed", detail msg = MIMEMultipart("alternative") msg.attach(MIMEText(text_content, "plain", "utf-8")) msg.attach(MIMEText(html_content, "html", "utf-8")) msg["Subject"] = subject if settings.get("from_name"): msg["From"] = formataddr((settings["from_name"], settings["from_email"])) else: msg["From"] = settings["from_email"] msg["To"] = settings["from_email"] msg["Bcc"] = ", ".join(validated_recipients) if settings.get("reply_to"): msg["Reply-To"] = settings["reply_to"] try: port = int(settings["port"]) with smtplib.SMTP(settings["host"], port, timeout=20) as server: if settings["use_tls"].lower() == "true": server.starttls() if settings["username"]: server.login(settings["username"], settings["password"]) server.sendmail(settings["from_email"], validated_recipients, msg.as_string()) detail = f"Versand erfolgreich an {len(validated_recipients)} Empfänger." _log(db, subject, validated_recipients, "sent", detail, scheduled) return "sent", detail except Exception as exc: logger.exception("SMTP-Versand fehlgeschlagen") detail = "Versandfehler: E-Mail konnte nicht zugestellt werden. Details stehen im Server-Log." _log(db, subject, validated_recipients, "failed", detail, scheduled) return "failed", detail def mark_scheduled_sent(db: Session, now: datetime) -> None: from app.services.config_store import set_config set_config(db, "smtp.last_scheduled_date", now.strftime("%Y-%m-%d")) def _log(db: Session, subject: str, recipients: Sequence[str], status: str, detail: str, scheduled: bool) -> None: db.add( SendLog( subject=subject, recipients=", ".join(recipients) if recipients else "-", status=status, detail=detail, scheduled=scheduled, ) ) db.commit()