Copy directly-sent e-mails to an IMAP Sent folder, with a live folder picker

A raw SMTP send has no server-side "Sent" copy on its own (unlike IMAP
drafts, which are inherently server-side) -- add an opt-in checkbox plus a
configurable target folder so directly sent Mahnungen/SEPA-info-mails still
show up in the account's Gesendet/Sent folder like a normal mail client
would leave them. Applies only to "send" delivery; drafts already live on
the server by definition.

Both the Entwürfe- and Gesendet-folder fields are now editable comboboxes:
a new "Ordnerliste laden" button fetches the real folder list from the IMAP
server (needs working credentials first) via LIST, decoding folder names
from modified UTF-7 (RFC 3501) so names like "Entwürfe" render correctly
instead of as "Entw&APw-rfe". Free text still works -- ensure_imap_folder()
creates the folder on first use if it doesn't exist yet, checked once per
batch rather than before every single message.

mail_delivery.append_to_imap_drafts() became the more general
append_message(client, content, folder=, flags=), reused for both the
\Draft and \Seen cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-08-15 04:34:34 +02:00
co-authored by Claude Sonnet 5
parent 6b0da82b45
commit cc4aaef895
9 changed files with 505 additions and 28 deletions
+14
View File
@@ -154,6 +154,10 @@ DEFAULT_CONFIGURATION = {
"imap_username": "",
"imap_password": "",
"imap_drafts_folder": "INBOX.Entwürfe",
# Sent-copy applies only to "send" (a direct SMTP send has no server-side
# "Sent" copy on its own, unlike drafts which are inherently server-side).
"imap_sent_enabled": False,
"imap_sent_folder": "INBOX.Sent",
},
}
@@ -2660,6 +2664,8 @@ class MemberRepository:
"imap_drafts_folder": str(
settings.get("imap_drafts_folder", defaults["imap_drafts_folder"])
),
"imap_sent_enabled": bool(settings.get("imap_sent_enabled", defaults["imap_sent_enabled"])),
"imap_sent_folder": str(settings.get("imap_sent_folder", defaults["imap_sent_folder"])),
}
def save_email_settings(
@@ -2677,6 +2683,8 @@ class MemberRepository:
imap_username: str,
imap_password: str,
imap_drafts_folder: str,
imap_sent_enabled: bool = False,
imap_sent_folder: str = "",
) -> None:
if delivery_mode not in {"local", "send", "drafts", "ask"}:
raise RepositoryError("Ungültiger Versandmodus.")
@@ -2688,6 +2696,10 @@ class MemberRepository:
raise RepositoryError("Für den direkten Versand ist ein SMTP-Server erforderlich.")
if delivery_mode in {"drafts", "ask"} and not imap_host.strip():
raise RepositoryError("Für die Ablage als Entwurf ist ein IMAP-Server erforderlich.")
if imap_sent_enabled and not imap_host.strip():
raise RepositoryError(
"Für die Ablage im Gesendet-Ordner ist ein IMAP-Server erforderlich."
)
for label, port in (("Der SMTP-Port", smtp_port), ("Der IMAP-Port", imap_port)):
if port < 1 or port > 65535:
raise RepositoryError(f"{label} muss zwischen 1 und 65535 liegen.")
@@ -2705,6 +2717,8 @@ class MemberRepository:
"imap_username": imap_username.strip(),
"imap_password": imap_password,
"imap_drafts_folder": imap_drafts_folder.strip() or "INBOX.Entwürfe",
"imap_sent_enabled": bool(imap_sent_enabled),
"imap_sent_folder": imap_sent_folder.strip() or "INBOX.Sent",
}
write_json_atomic(self.root / "repository.json", config)