diff --git a/supabase/functions/receive-inbound-email/index.ts b/supabase/functions/receive-inbound-email/index.ts index cd4f95c8..fb5a6696 100644 --- a/supabase/functions/receive-inbound-email/index.ts +++ b/supabase/functions/receive-inbound-email/index.ts @@ -47,107 +47,176 @@ const handler = async (req: Request): Promise => { let threadId = headers['X-Thread-ID'] || (inReplyTo ? inReplyTo.replace(/<|>/g, '').split('@')[0] : null); - if (!threadId) { - edgeLogger.warn('Email missing thread ID', { + // If no thread ID, this is a NEW direct email (not a reply) + const isNewEmail = !threadId; + + if (isNewEmail) { + edgeLogger.info('New direct email received (no thread ID)', { requestId: tracking.requestId, + from, + subject, messageId }); - return new Response(JSON.stringify({ success: false, reason: 'no_thread_id' }), { - status: 200, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - }); } - // Extract ticket number from thread_id (handles multiple formats) - // Formats: "TW-100000.uuid", "ticket-TW-100000", "TW-100000" - const ticketMatch = threadId.match(/(?:ticket-)?(TW-\d+)/i); - const ticketNumber = ticketMatch ? ticketMatch[1] : null; - - edgeLogger.info('Thread ID extracted', { - requestId: tracking.requestId, - rawThreadId: threadId, - ticketNumber - }); - - // Find submission by thread_id or ticket_number + // Find or create submission let submission = null; let submissionError = null; - // Strategy 1: Try exact thread_id match - const { data: submissionByThreadId, error: error1 } = await supabase - .from('contact_submissions') - .select('id, email, status, ticket_number') - .eq('thread_id', threadId) - .maybeSingle(); - - if (submissionByThreadId) { - submission = submissionByThreadId; - } else if (ticketNumber) { - // Strategy 2: Try ticket_number match - const { data: submissionByTicket, error: error2 } = await supabase + if (isNewEmail) { + // Extract sender email + const senderEmail = from.match(/<(.+)>/)?.[1] || from; + const senderName = from.match(/^(.+?)\s*/)?.[1] || from; - if (senderEmail.toLowerCase() !== submission.email.toLowerCase()) { - edgeLogger.warn('Sender email mismatch', { - requestId: tracking.requestId, - expected: submission.email, - received: senderEmail - }); - return new Response(JSON.stringify({ success: false, reason: 'email_mismatch' }), { - status: 200, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - }); + // Strategy 1: Try exact thread_id match + const { data: submissionByThreadId, error: error1 } = await supabase + .from('contact_submissions') + .select('id, email, status, ticket_number') + .eq('thread_id', threadId) + .maybeSingle(); + + if (submissionByThreadId) { + submission = submissionByThreadId; + } else if (ticketNumber) { + // Strategy 2: Try ticket_number match + const { data: submissionByTicket, error: error2 } = await supabase + .from('contact_submissions') + .select('id, email, status, ticket_number, thread_id') + .eq('ticket_number', ticketNumber) + .maybeSingle(); + + if (submissionByTicket) { + submission = submissionByTicket; + + // Update thread_id if it's null or in old format + if (!submissionByTicket.thread_id || submissionByTicket.thread_id !== threadId) { + await supabase + .from('contact_submissions') + .update({ thread_id: threadId }) + .eq('id', submissionByTicket.id); + + edgeLogger.info('Updated submission thread_id', { + requestId: tracking.requestId, + submissionId: submissionByTicket.id, + oldThreadId: submissionByTicket.thread_id, + newThreadId: threadId + }); + } + } else { + submissionError = error2; + } + } else { + submissionError = error1; + } + + if (submissionError || !submission) { + edgeLogger.warn('Submission not found for thread ID', { + requestId: tracking.requestId, + threadId, + ticketNumber, + error: submissionError + }); + return new Response(JSON.stringify({ success: false, reason: 'submission_not_found' }), { + status: 200, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + }); + } + + // Verify sender email matches (only for existing submissions) + const senderEmail = from.match(/<(.+)>/)?.[1] || from; + if (senderEmail.toLowerCase() !== submission.email.toLowerCase()) { + edgeLogger.warn('Sender email mismatch', { + requestId: tracking.requestId, + expected: submission.email, + received: senderEmail + }); + return new Response(JSON.stringify({ success: false, reason: 'email_mismatch' }), { + status: 200, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + }); + } } // Insert email thread record + const senderEmail = from.match(/<(.+)>/)?.[1] || from; const { error: insertError } = await supabase .from('contact_email_threads') .insert({ submission_id: submission.id, message_id: messageId, - in_reply_to: inReplyTo, + in_reply_to: inReplyTo || null, reference_chain: references || [], from_email: senderEmail, to_email: to, @@ -157,7 +226,8 @@ const handler = async (req: Request): Promise => { direction: 'inbound', metadata: { received_at: new Date().toISOString(), - headers: headers + headers: headers, + is_new_ticket: isNewEmail } });