added amazon importer and logging smtp

This commit is contained in:
2026-03-20 16:22:38 +01:00
parent 9fdada5dbe
commit a4e39332c7
16 changed files with 2619 additions and 255 deletions
+182 -112
View File
@@ -9,11 +9,30 @@ from email.mime.text import MIMEText
from email import encoders
import logging
from app.database import get_settings, add_log_entry
from app.database import get_settings, add_log_entry, get_import_email
logger = logging.getLogger(__name__)
def _send_with_log(smtp_conn: smtplib.SMTP, msg) -> str:
"""Send email and capture SMTP protocol exchange."""
log_lines = []
original_print_debug = smtp_conn._print_debug
def _capture(*args):
log_lines.append(" ".join(str(a) for a in args))
smtp_conn._print_debug = _capture
old_level = smtp_conn.debuglevel
smtp_conn.set_debuglevel(1)
try:
smtp_conn.send_message(msg)
finally:
smtp_conn.set_debuglevel(old_level)
smtp_conn._print_debug = original_print_debug
return "\n".join(log_lines)
def _connect_imap(settings: dict) -> imaplib.IMAP4_SSL | imaplib.IMAP4:
server = settings["imap_server"]
port = int(settings.get("imap_port", 993))
@@ -118,21 +137,117 @@ def _move_email(conn: imaplib.IMAP4, msg_uid: bytes, dest_folder: str):
conn.expunge()
async def process_mailbox() -> dict:
settings = await get_settings()
if not settings.get("imap_server") or not settings.get("import_email"):
logger.warning("IMAP oder Import-Email nicht konfiguriert")
return {"processed": 0, "skipped": 0, "errors": 0, "error": "Nicht konfiguriert"}
source_folder = settings.get("source_folder", "INBOX")
processed_folder = settings.get("processed_folder", "INBOX/Verarbeitet")
import_email = settings["import_email"]
async def _process_folder(
imap_conn, smtp_conn, settings: dict,
source_folder: str, processed_folder: str,
import_email: str, beleg_type: str, fetch_since: str,
) -> dict:
"""Process one IMAP folder pair. Returns counts dict."""
smtp_from = settings.get("smtp_username", "")
processed = 0
skipped = 0
errors = 0
_ensure_folder_exists(imap_conn, processed_folder)
status, _ = imap_conn.select(f'"{source_folder}"')
if status != "OK":
logger.warning(f"Ordner '{source_folder}' konnte nicht geöffnet werden")
return {"processed": 0, "skipped": 0, "errors": 0}
search_criteria = "ALL"
if fetch_since:
try:
from datetime import datetime
dt = datetime.strptime(fetch_since, "%Y-%m-%d")
imap_date = dt.strftime("%d-%b-%Y")
search_criteria = f'(SINCE {imap_date})'
except ValueError:
logger.warning(f"Ungültiges Datum: {fetch_since}, verwende ALL")
status, data = imap_conn.uid("SEARCH", None, search_criteria)
if status != "OK" or not data[0]:
logger.info(f"Keine Emails im Ordner '{source_folder}' ({beleg_type})")
return {"processed": 0, "skipped": 0, "errors": 0}
msg_uids = data[0].split()
logger.info(f"{len(msg_uids)} Email(s) im Ordner '{source_folder}' ({beleg_type})")
for msg_uid in msg_uids:
subject = "?"
from_addr = "?"
try:
status, msg_data = imap_conn.uid("FETCH", msg_uid, "(RFC822)")
if status != "OK":
continue
raw_email = msg_data[0][1]
msg = email.message_from_bytes(raw_email, policy=policy.default)
subject = str(msg.get("Subject", "(Kein Betreff)"))
from_addr = str(msg.get("From", "(Unbekannt)"))
attachments = _extract_attachments(msg)
if not attachments:
skipped += 1
logger.debug(f"Übersprungen (keine Anhänge): {subject}")
continue
forward_msg = _build_forward_email(
from_addr=smtp_from,
to_addr=import_email,
original_subject=subject,
original_from=from_addr,
attachments=attachments,
)
smtp_log = _send_with_log(smtp_conn, forward_msg)
imap_conn.select(f'"{source_folder}"')
_move_email(imap_conn, msg_uid, processed_folder)
imap_conn.select(f'"{source_folder}"')
processed += 1
logger.info(f"Verarbeitet ({beleg_type}): {subject} ({len(attachments)} Anhänge)")
await add_log_entry(
email_subject=subject,
email_from=from_addr,
attachments_count=len(attachments),
status="success",
sent_to=import_email,
smtp_log=smtp_log,
beleg_type=beleg_type,
)
except Exception as e:
errors += 1
logger.error(f"Fehler bei Email UID {msg_uid}: {e}")
try:
await add_log_entry(
email_subject=subject,
email_from=from_addr,
attachments_count=0,
status="error",
error_message=str(e),
beleg_type=beleg_type,
)
except Exception:
pass
return {"processed": processed, "skipped": skipped, "errors": errors}
async def process_mailbox() -> dict:
settings = await get_settings()
import_email_eingang = get_import_email(settings, "eingang")
if not settings.get("imap_server") or not import_email_eingang:
logger.warning("IMAP oder Import-Email nicht konfiguriert")
return {"processed": 0, "skipped": 0, "errors": 0, "error": "Nicht konfiguriert"}
fetch_since = settings.get("fetch_since_date", "")
total = {"processed": 0, "skipped": 0, "errors": 0}
imap_conn = None
smtp_conn = None
@@ -140,92 +255,31 @@ async def process_mailbox() -> dict:
imap_conn = _connect_imap(settings)
smtp_conn = _connect_smtp(settings)
_ensure_folder_exists(imap_conn, processed_folder)
# Eingangsbelege
source = settings.get("source_folder", "INBOX")
processed_folder = settings.get("processed_folder", "INBOX/Verarbeitet")
result = await _process_folder(
imap_conn, smtp_conn, settings,
source, processed_folder,
import_email_eingang, "eingang", fetch_since,
)
for k in total:
total[k] += result[k]
status, _ = imap_conn.select(f'"{source_folder}"')
if status != "OK":
raise Exception(f"Ordner '{source_folder}' konnte nicht geöffnet werden")
# Build IMAP search criteria
search_criteria = "ALL"
fetch_since = settings.get("fetch_since_date", "")
if fetch_since:
try:
from datetime import datetime
dt = datetime.strptime(fetch_since, "%Y-%m-%d")
imap_date = dt.strftime("%d-%b-%Y")
search_criteria = f'(SINCE {imap_date})'
except ValueError:
logger.warning(f"Ungültiges Datum: {fetch_since}, verwende ALL")
status, data = imap_conn.uid("SEARCH", None, search_criteria)
if status != "OK" or not data[0]:
logger.info("Keine Emails im Ordner gefunden")
return {"processed": 0, "skipped": 0, "errors": 0}
msg_uids = data[0].split()
logger.info(f"{len(msg_uids)} Email(s) im Ordner '{source_folder}' gefunden")
for msg_uid in msg_uids:
try:
status, msg_data = imap_conn.uid("FETCH", msg_uid, "(RFC822)")
if status != "OK":
continue
raw_email = msg_data[0][1]
msg = email.message_from_bytes(raw_email, policy=policy.default)
subject = str(msg.get("Subject", "(Kein Betreff)"))
from_addr = str(msg.get("From", "(Unbekannt)"))
attachments = _extract_attachments(msg)
if not attachments:
skipped += 1
logger.debug(f"Übersprungen (keine Anhänge): {subject}")
continue
forward_msg = _build_forward_email(
from_addr=smtp_from,
to_addr=import_email,
original_subject=subject,
original_from=from_addr,
attachments=attachments,
)
smtp_conn.send_message(forward_msg)
# Re-select source folder before move (in case _ensure_folder changed it)
imap_conn.select(f'"{source_folder}"')
_move_email(imap_conn, msg_uid, processed_folder)
# Re-select after expunge to keep UIDs valid
imap_conn.select(f'"{source_folder}"')
processed += 1
logger.info(
f"Verarbeitet: {subject} ({len(attachments)} Anhänge)"
)
await add_log_entry(
email_subject=subject,
email_from=from_addr,
attachments_count=len(attachments),
status="success",
)
except Exception as e:
errors += 1
logger.error(f"Fehler bei Email UID {msg_uid}: {e}")
try:
await add_log_entry(
email_subject=subject if "subject" in dir() else "?",
email_from=from_addr if "from_addr" in dir() else "?",
attachments_count=0,
status="error",
error_message=str(e),
)
except Exception:
pass
# Ausgangsbelege (optional)
import_email_ausgang = get_import_email(settings, "ausgang")
source_ausgang = settings.get("source_folder_ausgang", "")
processed_ausgang = settings.get("processed_folder_ausgang", "")
if import_email_ausgang and source_ausgang:
if not processed_ausgang:
processed_ausgang = source_ausgang + "/Verarbeitet"
result = await _process_folder(
imap_conn, smtp_conn, settings,
source_ausgang, processed_ausgang,
import_email_ausgang, "ausgang", fetch_since,
)
for k in total:
total[k] += result[k]
except Exception as e:
logger.error(f"Verbindungsfehler: {e}")
@@ -236,7 +290,7 @@ async def process_mailbox() -> dict:
status="error",
error_message=f"Verbindungsfehler: {e}",
)
return {"processed": processed, "skipped": skipped, "errors": errors + 1, "error": str(e)}
return {**total, "errors": total["errors"] + 1, "error": str(e)}
finally:
if imap_conn:
@@ -250,36 +304,52 @@ async def process_mailbox() -> dict:
except Exception:
pass
logger.info(
f"Fertig: {processed} verarbeitet, {skipped} übersprungen, {errors} Fehler"
)
return {"processed": processed, "skipped": skipped, "errors": errors}
logger.info(f"Fertig: {total['processed']} verarbeitet, {total['skipped']} übersprungen, {total['errors']} Fehler")
return total
async def send_test_email() -> dict:
settings = await get_settings()
if not settings.get("smtp_server") or not settings.get("import_email"):
return {"success": False, "error": "SMTP oder Import-Email nicht konfiguriert"}
import_email_eingang = get_import_email(settings, "eingang")
import_email_ausgang = get_import_email(settings, "ausgang")
if not settings.get("smtp_server") or not import_email_eingang:
return {"success": False, "error": "SMTP oder Import-Email (Eingang) nicht konfiguriert"}
try:
smtp_conn = _connect_smtp(settings)
smtp_logs = []
# Test Eingangsbelege
msg = MIMEMultipart()
msg["From"] = settings["smtp_username"]
msg["To"] = settings["import_email"]
msg["Subject"] = "Belegimport - Test-Email"
msg["To"] = import_email_eingang
msg["Subject"] = "Belegimport - Test-Email (Eingangsbelege)"
msg.attach(MIMEText(
"Dies ist eine Test-Email vom Belegimport Service.\n"
"Wenn Sie diese Email erhalten, funktioniert die SMTP-Verbindung.",
"plain",
"utf-8",
"Ziel: Eingangsbelege",
"plain", "utf-8",
))
smtp_logs.append("=== Eingangsbelege ===")
smtp_logs.append(_send_with_log(smtp_conn, msg))
# Test Ausgangsbelege (if configured)
if import_email_ausgang:
msg2 = MIMEMultipart()
msg2["From"] = settings["smtp_username"]
msg2["To"] = import_email_ausgang
msg2["Subject"] = "Belegimport - Test-Email (Ausgangsbelege)"
msg2.attach(MIMEText(
"Dies ist eine Test-Email vom Belegimport Service.\n"
"Ziel: Ausgangsbelege",
"plain", "utf-8",
))
smtp_logs.append("=== Ausgangsbelege ===")
smtp_logs.append(_send_with_log(smtp_conn, msg2))
smtp_conn.send_message(msg)
smtp_conn.quit()
return {"success": True}
return {"success": True, "smtp_log": "\n".join(smtp_logs)}
except Exception as e:
logger.error(f"Test-Email fehlgeschlagen: {e}")