Feat: Alle ausgehenden Mails landen im IMAP-Gesendet-Ordner (SMTP + Brevo)

This commit is contained in:
rene 2026-05-15 12:32:52 +02:00
parent aea5f04bc1
commit 865407d428

View file

@ -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 <noreply@banyaro.app>")
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}")