( input: EmailConversationInput, threadService: ReturnType<typeof getThreadService> )
| 311 | // --- Thread resolution --- |
| 312 | |
| 313 | async function resolveThread( |
| 314 | input: EmailConversationInput, |
| 315 | threadService: ReturnType<typeof getThreadService> |
| 316 | ): Promise<Thread> { |
| 317 | // Tier 1: Look up In-Reply-To header in stored message IDs |
| 318 | // Verify sender matches thread owner to prevent thread hijacking via crafted headers |
| 319 | if (input.inReplyTo) { |
| 320 | const thread = await threadService.findThreadByEmailMessageId(input.inReplyTo); |
| 321 | if (thread && thread.user_id === input.senderEmail) { |
| 322 | logger.info({ threadId: thread.thread_id, inReplyTo: input.inReplyTo }, 'Found thread via In-Reply-To'); |
| 323 | return thread; |
| 324 | } |
| 325 | if (thread) { |
| 326 | logger.warn({ |
| 327 | threadId: thread.thread_id, |
| 328 | inReplyTo: input.inReplyTo, |
| 329 | threadOwner: thread.user_id, |
| 330 | sender: input.senderEmail, |
| 331 | }, 'In-Reply-To matched thread owned by different sender, ignoring'); |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | // Tier 2: Find recent thread from same sender with same subject |
| 336 | const thread = await threadService.findRecentEmailThread(input.senderEmail, input.subject); |
| 337 | if (thread) { |
| 338 | logger.info({ threadId: thread.thread_id, senderEmail: input.senderEmail }, 'Found thread via recent sender + subject'); |
| 339 | return thread; |
| 340 | } |
| 341 | |
| 342 | // Tier 3: Create new thread |
| 343 | // Always use sender email as user_id (not WorkOS ID) since email identity is unverified |
| 344 | const newThread = await threadService.getOrCreateThread({ |
| 345 | channel: 'email', |
| 346 | external_id: `email:${input.messageId}`, |
| 347 | user_type: 'anonymous', |
| 348 | user_id: input.senderEmail, |
| 349 | user_display_name: input.senderDisplayName, |
| 350 | context: { |
| 351 | sender_email: input.senderEmail, |
| 352 | subject: input.subject, |
| 353 | }, |
| 354 | title: input.subject, |
| 355 | }); |
| 356 | |
| 357 | logger.info({ threadId: newThread.thread_id, senderEmail: input.senderEmail }, 'Created new email thread'); |
| 358 | return newThread; |
| 359 | } |
| 360 | |
| 361 | // --- System context --- |
| 362 |
no test coverage detected