Mail list: name everyone who wrote in the thread, and call the user "me"
The sender column showed participants[0], and participants are rebuilt
newest message first — so replying to a thread put the user in that slot
and the list named them instead of the person who wrote in.
Thread summaries (list and search) now carry `senders`: the distinct From
addresses of the thread's visible messages, oldest first, with the same
Trash/Spam visibility as the message count. The list joins them and shows
the user's own account addresses as "me" ("me, Alice" for a thread the
user started), and the avatar prefers the other side of the conversation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,11 +37,17 @@ class ApiCtx:
|
||||
|
||||
|
||||
def _raw(
|
||||
subject: str, body: str, *, mid: str, refs: str | None = None, html: str | None = None
|
||||
subject: str,
|
||||
body: str,
|
||||
*,
|
||||
mid: str,
|
||||
refs: str | None = None,
|
||||
html: str | None = None,
|
||||
sender: str = "Alice <alice@example.com>",
|
||||
) -> bytes:
|
||||
m = EmailMessage()
|
||||
m["Subject"] = subject
|
||||
m["From"] = "Alice <alice@example.com>"
|
||||
m["From"] = sender
|
||||
m["To"] = "me@example.com"
|
||||
m["Message-ID"] = mid
|
||||
m["Date"] = "Mon, 09 Mar 2026 08:36:37 +0100"
|
||||
@@ -159,6 +165,40 @@ async def test_list_threads(api: ApiCtx) -> None:
|
||||
assert kickoff["message_count"] == 2
|
||||
|
||||
|
||||
async def test_list_senders_are_authors_oldest_first(api: ApiCtx) -> None:
|
||||
# Our reply joins the kickoff thread: the sender column lists both authors in
|
||||
# the order they wrote — Alice twice collapses to one entry, and the recipients
|
||||
# (me, on Alice's messages) are not senders.
|
||||
assert api.session is not None and api.account_id is not None
|
||||
inbox = await api.session.scalar(
|
||||
select(Mailbox).where(Mailbox.account_id == api.account_id, Mailbox.path == "INBOX")
|
||||
)
|
||||
assert inbox is not None
|
||||
raw = _raw(
|
||||
"Re: Project kickoff",
|
||||
"thanks",
|
||||
mid="<d@x.com>",
|
||||
refs="<b@x.com>",
|
||||
sender="Me <me@example.com>",
|
||||
)
|
||||
remote = RemoteMessage(uid=9, raw=raw, flags=frozenset(), internal_date=datetime.now(UTC))
|
||||
await persist_message(
|
||||
api.session,
|
||||
user_id=api.user_id,
|
||||
account_id=api.account_id,
|
||||
mailbox_id=inbox.id,
|
||||
mailbox_special_use="inbox",
|
||||
remote=remote,
|
||||
)
|
||||
await api.session.commit()
|
||||
|
||||
resp = await api.client.get("/api/threads")
|
||||
kickoff = next(t for t in resp.json()["threads"] if "kickoff" in t["subject"].lower())
|
||||
assert [a["email"] for a in kickoff["senders"]] == ["alice@example.com", "me@example.com"]
|
||||
lunch = next(t for t in resp.json()["threads"] if t["subject"] == "Lunch")
|
||||
assert [a["email"] for a in lunch["senders"]] == ["alice@example.com"]
|
||||
|
||||
|
||||
async def test_folder_view(api: ApiCtx) -> None:
|
||||
# all seeded mail is in an inbox mailbox
|
||||
inbox = await api.client.get("/api/threads", params={"view": "folder:inbox"})
|
||||
|
||||
Reference in New Issue
Block a user