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)
|
2. SMTP — wenn SMTP_HOST gesetzt (Fallback)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import imaplib
|
||||||
import os
|
import os
|
||||||
import base64
|
import base64
|
||||||
import smtplib
|
import smtplib
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import ssl
|
||||||
|
from datetime import datetime
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.application import MIMEApplication
|
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_USER = os.getenv("SMTP_USER", "")
|
||||||
SMTP_PASS = os.getenv("SMTP_PASS", "") or os.getenv("SMTP_SUPPORT_PASS", "")
|
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>")
|
SMTP_FROM = os.getenv("SMTP_FROM", "Ban Yaro <noreply@banyaro.app>")
|
||||||
APP_URL = os.getenv("APP_URL", "https://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
|
# 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["From"] = SMTP_FROM
|
||||||
msg["To"] = to
|
msg["To"] = to
|
||||||
|
|
||||||
|
msg_bytes = msg.as_bytes()
|
||||||
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as s:
|
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as s:
|
||||||
s.ehlo()
|
s.ehlo()
|
||||||
s.starttls()
|
s.starttls()
|
||||||
if SMTP_USER:
|
if SMTP_USER:
|
||||||
s.login(SMTP_USER, SMTP_PASS)
|
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:
|
try:
|
||||||
await _send_brevo(to, subject, html, plain, attachments)
|
await _send_brevo(to, subject, html, plain, attachments)
|
||||||
logger.info(f"Mail via Brevo gesendet: «{subject}» → {to}")
|
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
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Brevo-Fehler: {e}")
|
logger.error(f"Brevo-Fehler: {e}")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue