diff --git a/backend/mailer.py b/backend/mailer.py index f81fb33..c29f105 100644 --- a/backend/mailer.py +++ b/backend/mailer.py @@ -5,11 +5,14 @@ Unterstützt zwei Backends (wird automatisch gewählt): 2. SMTP — wenn SMTP_HOST gesetzt (Fallback) """ +import imaplib import os import base64 import smtplib import asyncio import logging +import ssl +from datetime import datetime from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication @@ -28,10 +31,70 @@ SMTP_PORT = int(os.getenv("SMTP_PORT", "587")) SMTP_USER = os.getenv("SMTP_USER", "") SMTP_PASS = os.getenv("SMTP_PASS", "") or os.getenv("SMTP_SUPPORT_PASS", "") +# IMAP für Gesendet-Ordner +IMAP_HOST = os.getenv("IMAP_HOST", SMTP_HOST) +IMAP_PORT = int(os.getenv("IMAP_PORT", "993")) +_SENT_CANDIDATES = ["Sent", "Sent Messages", "Sent Items", "INBOX.Sent", "Gesendete Objekte"] + SMTP_FROM = os.getenv("SMTP_FROM", "Ban Yaro ") APP_URL = os.getenv("APP_URL", "https://banyaro.app") +# ------------------------------------------------------------------ +# IMAP: Mail in Gesendet-Ordner speichern +# ------------------------------------------------------------------ +def _imap_save_sent(msg_bytes: bytes): + if not IMAP_HOST or not SMTP_USER or not SMTP_PASS: + return + try: + ctx = ssl.create_default_context() + with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, ssl_context=ctx) as imap: + imap.login(SMTP_USER, SMTP_PASS) + _, raw_folders = imap.list() + available = [f.decode(errors="replace") for f in (raw_folders or [])] + 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" + imap.append( + folder, + r"\Seen", + imaplib.Time2Internaldate(datetime.now().timestamp()), + msg_bytes, + ) + except Exception as e: + logger.warning(f"IMAP Gesendet-Speicherung fehlgeschlagen: {e}") + + +def _build_mime_copy(to: str, subject: str, html: str, plain: str, attachments: list | None) -> MIMEMultipart: + """Baut eine MIME-Nachricht für die Gesendet-Ablage (Brevo-Pfad).""" + if attachments: + msg = MIMEMultipart("mixed") + alt = MIMEMultipart("alternative") + alt.attach(MIMEText(plain, "plain", "utf-8")) + alt.attach(MIMEText(html, "html", "utf-8")) + msg.attach(alt) + for a in attachments: + part = MIMEApplication(a["content"], Name=a["filename"]) + part["Content-Disposition"] = f'attachment; filename="{a["filename"]}"' + msg.attach(part) + else: + msg = MIMEMultipart("alternative") + msg.attach(MIMEText(plain, "plain", "utf-8")) + msg.attach(MIMEText(html, "html", "utf-8")) + msg["Subject"] = subject + msg["From"] = SMTP_FROM + msg["To"] = to + return msg + + # ------------------------------------------------------------------ # Brevo REST-API # ------------------------------------------------------------------ @@ -92,12 +155,14 @@ def _send_smtp_sync(to: str, subject: str, html: str, plain: str, attachments: l msg["From"] = SMTP_FROM msg["To"] = to + msg_bytes = msg.as_bytes() with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as s: s.ehlo() s.starttls() if SMTP_USER: s.login(SMTP_USER, SMTP_PASS) - s.sendmail(SMTP_FROM, [to], msg.as_string()) + s.sendmail(SMTP_FROM, [to], msg_bytes) + _imap_save_sent(msg_bytes) # ------------------------------------------------------------------ @@ -108,6 +173,11 @@ async def send_email(to: str, subject: str, html: str, plain: str = "", attachme try: await _send_brevo(to, subject, html, plain, attachments) logger.info(f"Mail via Brevo gesendet: «{subject}» → {to}") + # MIME-Kopie für Gesendet-Ordner konstruieren + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, lambda: _imap_save_sent( + _build_mime_copy(to, subject, html, plain, attachments).as_bytes() + )) return except Exception as e: logger.error(f"Brevo-Fehler: {e}")