fix(projekt-matching): store outgoing mail copies in Sent via IMAP append
This commit is contained in:
@@ -71,6 +71,39 @@ class MailBox:
|
|||||||
pass
|
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):
|
def send_mail(user, password, to, subject, body):
|
||||||
msg = EmailMessage()
|
msg = EmailMessage()
|
||||||
msg["From"], msg["To"], msg["Subject"] = user, to, subject
|
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.starttls(context=ssl.create_default_context())
|
||||||
server.login(user, password)
|
server.login(user, password)
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
|
try:
|
||||||
|
append_sent(user, password, msg)
|
||||||
|
except Exception: # noqa: BLE001 — copy is best effort; the mail went out
|
||||||
|
pass
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import email
|
import email
|
||||||
|
from email.message import EmailMessage
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
@@ -62,7 +63,8 @@ def test_fetch_raises_on_malformed_ok_response():
|
|||||||
|
|
||||||
|
|
||||||
def test_send_mail_starttls():
|
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
|
server = smtp.return_value.__enter__.return_value
|
||||||
mailer.send_mail("u@x", "pw", "to@x", "Subj", "Body")
|
mailer.send_mail("u@x", "pw", "to@x", "Subj", "Body")
|
||||||
server.starttls.assert_called_once()
|
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["Subject"] == "Subj" and msg["To"] == "to@x"
|
||||||
assert msg["Date"]
|
assert msg["Date"]
|
||||||
assert msg["Message-ID"].endswith("@destengs.com>")
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user