diff --git a/projekt-matching/projektmatch/mailer.py b/projekt-matching/projektmatch/mailer.py index 9dbdd0b..4ed2a09 100644 --- a/projekt-matching/projektmatch/mailer.py +++ b/projekt-matching/projektmatch/mailer.py @@ -71,6 +71,39 @@ class MailBox: pass +def _sent_folder(conn): + """Find the server's Sent folder via special-use flag, fallback names.""" + typ, data = conn.list() + candidates = [] + for raw in data or []: + line = raw.decode() if isinstance(raw, bytes) else str(raw) + name = line.rsplit(" ", 1)[-1].strip('"') + if "\\Sent" in line.split(")")[0]: + return name + candidates.append(name) + for cand in ("Sent", "INBOX.Sent"): + if cand in candidates: + return cand + return "Sent" + + +def append_sent(user, password, msg, conn=None): + """Store a copy of an outgoing message in the Sent folder (best effort).""" + own = conn is None + conn = conn or imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) + try: + if own: + conn.login(user, password) + folder = _sent_folder(conn) + conn.append(folder, "(\\Seen)", None, msg.as_bytes()) + finally: + if own: + try: + conn.logout() + except Exception: + pass + + def send_mail(user, password, to, subject, body): msg = EmailMessage() msg["From"], msg["To"], msg["Subject"] = user, to, subject @@ -81,3 +114,7 @@ def send_mail(user, password, to, subject, body): server.starttls(context=ssl.create_default_context()) server.login(user, password) server.send_message(msg) + try: + append_sent(user, password, msg) + except Exception: # noqa: BLE001 — copy is best effort; the mail went out + pass diff --git a/projekt-matching/tests/test_mailer.py b/projekt-matching/tests/test_mailer.py index b29227a..a1e4119 100644 --- a/projekt-matching/tests/test_mailer.py +++ b/projekt-matching/tests/test_mailer.py @@ -1,4 +1,5 @@ import email +from email.message import EmailMessage import pytest from unittest import mock @@ -62,7 +63,8 @@ def test_fetch_raises_on_malformed_ok_response(): def test_send_mail_starttls(): - with mock.patch("projektmatch.mailer.smtplib.SMTP") as smtp: + with mock.patch("projektmatch.mailer.smtplib.SMTP") as smtp, \ + mock.patch("projektmatch.mailer.append_sent") as append_sent: server = smtp.return_value.__enter__.return_value mailer.send_mail("u@x", "pw", "to@x", "Subj", "Body") server.starttls.assert_called_once() @@ -71,3 +73,30 @@ def test_send_mail_starttls(): assert msg["Subject"] == "Subj" and msg["To"] == "to@x" assert msg["Date"] assert msg["Message-ID"].endswith("@destengs.com>") + append_sent.assert_called_once_with("u@x", "pw", msg) + + +def test_send_mail_survives_append_failure(): + with mock.patch("projektmatch.mailer.smtplib.SMTP") as smtp, \ + mock.patch("projektmatch.mailer.append_sent", + side_effect=OSError("imap down")): + mailer.send_mail("u@x", "pw", "to@x", "Subj", "Body") + + +def test_append_sent_uses_special_use_folder(): + conn = mock.Mock() + conn.list.return_value = ("OK", [ + b'(\\HasNoChildren) "." "INBOX"', + b'(\\HasNoChildren \\Sent) "." "Sent"']) + msg = EmailMessage() + msg["Subject"] = "Subj" + msg.set_content("Body") + + mailer.append_sent("u@x", "pw", msg, conn=conn) + + args = conn.append.call_args.args + assert args[0] == "Sent" + assert "\\Seen" in args[1] + assert args[3] == msg.as_bytes() + conn.login.assert_not_called() + conn.logout.assert_not_called()