Skip to content

Commit bac784d

Browse files
bchapuisclaude
andcommitted
Test mailbox thread search address isolation
Guard the search path against the SQL precedence trap where an OR'd LIKE clause could leak threads from other addresses, and confirm typed LIKE wildcards are matched literally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a0e3b45 commit bac784d

1 file changed

Lines changed: 45 additions & 0 deletions

File tree

apps/api/src/durable-objects/mailbox-do.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,51 @@ describe("MailboxDO", () => {
173173
expect(elsewhere[0].subject).toBe("Elsewhere");
174174
});
175175

176+
it("scopes thread search to the address, even when another address matches", async () => {
177+
// A thread on a *different* address whose sender matches the search term.
178+
// If the email_id filter and the OR'd LIKE clauses were not parenthesized,
179+
// this would leak into the searched address's results.
180+
await mailbox.ingestInbound(
181+
inbound({
182+
emailId: "email-other",
183+
subject: "Unrelated subject",
184+
fromEmail: "alice@example.com",
185+
})
186+
);
187+
const { threadId: mine } = await mailbox.ingestInbound(
188+
inbound({ subject: "Quarterly report", fromEmail: "alice@example.com" })
189+
);
190+
// A second thread on our address that should NOT match "alice".
191+
await mailbox.ingestInbound(
192+
inbound({ subject: "Nothing to see", fromEmail: "bob@example.com" })
193+
);
194+
195+
// Match by sender — only our address's matching thread is returned.
196+
const bySender = await mailbox.listThreads(EMAIL_ID, 100, 0, "alice");
197+
expect(bySender.map((t) => t.id)).toEqual([mine]);
198+
199+
// Match by subject — same isolation.
200+
const bySubject = await mailbox.listThreads(EMAIL_ID, 100, 0, "report");
201+
expect(bySubject.map((t) => t.id)).toEqual([mine]);
202+
203+
// The other address can still find its own thread by the same sender.
204+
const other = await mailbox.listThreads("email-other", 100, 0, "alice");
205+
expect(other).toHaveLength(1);
206+
expect(other[0].subject).toBe("Unrelated subject");
207+
});
208+
209+
it("treats typed LIKE wildcards as literals in search", async () => {
210+
const { threadId: literal } = await mailbox.ingestInbound(
211+
inbound({ subject: "50% off sale" })
212+
);
213+
await mailbox.ingestInbound(inbound({ subject: "plain subject" }));
214+
215+
// "%" must match a literal percent sign, not act as a wildcard (which
216+
// would otherwise also return "plain subject").
217+
const results = await mailbox.listThreads(EMAIL_ID, 100, 0, "50%");
218+
expect(results.map((t) => t.id)).toEqual([literal]);
219+
});
220+
176221
it("lists and fetches attachments scoped to a thread", async () => {
177222
const messageId = uuidv7();
178223
const attachmentId = uuidv7();

0 commit comments

Comments
 (0)