This makes it possible to exclude specific editors (e.g. Aranzinger) from edited-article results by default, including dashboard generation and scheduled SMTP runs. Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
6.1 KiB
Python
155 lines
6.1 KiB
Python
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()
|