Skip to content

Commit 1df2f91

Browse files
theetherGitclaude
andcommitted
fix(mail): drop misdirected iTIP replies for organizers not in our DB
REPLY-class methods (REPLY/COUNTER/DECLINECOUNTER/REFRESH) are directed at the organizer. If the ICS organizer isn't an active mailbox in our org the reply is misdirected or spoofed — skip the calendar_event row and log in.invite_reply_unknown_organizer. No bounce (machine reply = loop risk). REQUEST/CANCEL/PUBLISH unaffected; recipient is already SMTP-validated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bc49396 commit 1df2f91

1 file changed

Lines changed: 28 additions & 1 deletion

File tree

‎packages/mail-core/src/queue-consumer.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: Apache-2.0
2-
import { eq } from "drizzle-orm";
2+
import { eq, and } from "drizzle-orm";
33
import { drizzle } from "drizzle-orm/d1";
44
import PostalMime from "postal-mime";
55
import * as schema from "@doota/db/schema";
@@ -211,9 +211,36 @@ async function persistInvite(
211211
// Provider's own Yes/Maybe/No links (message-level) — same for every event.
212212
const rsvpLinks = extractRsvpLinks(parsed.html);
213213
const isCancelMethod = cal.method === "CANCEL";
214+
// iTIP methods directed AT the organizer (attendee → organizer). They're only
215+
// actionable if the organizer is one of OUR mailboxes; otherwise the reply is
216+
// misdirected or spoofed — drop it (never bounce a machine-generated reply, it
217+
// risks a mail loop).
218+
const isReplyClass = ["REPLY", "COUNTER", "DECLINECOUNTER", "REFRESH"].includes(
219+
cal.method ?? "",
220+
);
214221

215222
for (const ev of cal.events) {
216223
if (!ev.uid) continue; // can't dedupe/RSVP without a UID — skip (raw kept)
224+
if (isReplyClass) {
225+
const organizer = ev.organizer.email?.toLowerCase() ?? "";
226+
const ours =
227+
organizer &&
228+
(await db.query.mailbox.findFirst({
229+
where: and(
230+
eq(schema.mailbox.orgId, orgId),
231+
eq(schema.mailbox.address, organizer),
232+
),
233+
columns: { id: true },
234+
}));
235+
if (!ours) {
236+
log.warn("in.invite_reply_unknown_organizer", {
237+
messageId,
238+
uid: ev.uid,
239+
organizer: organizer || null,
240+
});
241+
continue; // organizer not in our DB — reject/drop, don't store or act
242+
}
243+
}
217244
const detailsEnc = await encryptContent(
218245
ck,
219246
JSON.stringify({

0 commit comments

Comments
 (0)