Feat: Alle ausgehenden Mails landen im IMAP-Gesendet-Ordner (SMTP + Brevo)
This commit is contained in:
parent
aea5f04bc1
commit
865407d428
1 changed files with 71 additions and 1 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue