import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; import { createErrorResponse } from "../_shared/errorSanitizer.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; interface InboundEmailPayload { from: string; to: string; subject: string; text: string; html?: string; messageId: string; inReplyTo?: string; references?: string[]; headers: Record; } const handler = async (req: Request): Promise => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } const tracking = startRequest(); try { const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ); const payload: InboundEmailPayload = await req.json(); const { from, to, subject, text, html, messageId, inReplyTo, references, headers } = payload; edgeLogger.info('Inbound email received', { requestId: tracking.requestId, from, to, messageId }); // Extract thread ID from headers or inReplyTo let threadId = headers['X-Thread-ID'] || (inReplyTo ? inReplyTo.replace(/<|>/g, '').split('@')[0] : null); if (!threadId) { edgeLogger.warn('Email missing thread ID', { requestId: tracking.requestId, 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 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 .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 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 { error: insertError } = await supabase .from('contact_email_threads') .insert({ submission_id: submission.id, message_id: messageId, in_reply_to: inReplyTo, reference_chain: references || [], from_email: senderEmail, to_email: to, subject, body_text: text, body_html: html, direction: 'inbound', metadata: { received_at: new Date().toISOString(), headers: headers } }); if (insertError) { edgeLogger.error('Failed to insert inbound email thread', { requestId: tracking.requestId, error: insertError }); return createErrorResponse(insertError, 500, corsHeaders); } // Update submission status if pending if (submission.status === 'pending') { await supabase .from('contact_submissions') .update({ status: 'in_progress' }) .eq('id', submission.id); } edgeLogger.info('Inbound email processed successfully', { requestId: tracking.requestId, submissionId: submission.id, duration: endRequest(tracking) }); return new Response( JSON.stringify({ success: true }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } catch (error) { edgeLogger.error('Unexpected error in receive-inbound-email', { requestId: tracking.requestId, error: error instanceof Error ? error.message : String(error) }); return createErrorResponse(error, 500, corsHeaders); } }; serve(handler);