Refactor: Handle direct emails to admin

This commit is contained in:
gpt-engineer-app[bot]
2025-10-28 20:36:12 +00:00
parent 375db2e7d8
commit 41a3dcd02f

View File

@@ -47,107 +47,176 @@ const handler = async (req: Request): Promise<Response> => {
let threadId = headers['X-Thread-ID'] || let threadId = headers['X-Thread-ID'] ||
(inReplyTo ? inReplyTo.replace(/<|>/g, '').split('@')[0] : null); (inReplyTo ? inReplyTo.replace(/<|>/g, '').split('@')[0] : null);
if (!threadId) { // If no thread ID, this is a NEW direct email (not a reply)
edgeLogger.warn('Email missing thread ID', { const isNewEmail = !threadId;
if (isNewEmail) {
edgeLogger.info('New direct email received (no thread ID)', {
requestId: tracking.requestId, requestId: tracking.requestId,
from,
subject,
messageId 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) // Find or create submission
// 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 submission = null;
let submissionError = null; let submissionError = null;
// Strategy 1: Try exact thread_id match if (isNewEmail) {
const { data: submissionByThreadId, error: error1 } = await supabase // Extract sender email
.from('contact_submissions') const senderEmail = from.match(/<(.+)>/)?.[1] || from;
.select('id, email, status, ticket_number') const senderName = from.match(/^(.+?)\s*</)?.[1]?.trim() || senderEmail.split('@')[0];
.eq('thread_id', threadId)
.maybeSingle();
if (submissionByThreadId) { // Check for existing submission from this email in last 5 minutes (avoid duplicates)
submission = submissionByThreadId; const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
} else if (ticketNumber) { const { data: existingRecent } = await supabase
// Strategy 2: Try ticket_number match
const { data: submissionByTicket, error: error2 } = await supabase
.from('contact_submissions') .from('contact_submissions')
.select('id, email, status, ticket_number, thread_id') .select('id, ticket_number, thread_id, email')
.eq('ticket_number', ticketNumber) .eq('email', senderEmail.toLowerCase())
.eq('subject', subject || '(No Subject)')
.gte('created_at', fiveMinutesAgo)
.maybeSingle(); .maybeSingle();
if (submissionByTicket) { if (existingRecent) {
submission = submissionByTicket; // Use existing recent submission (duplicate email)
submission = existingRecent;
threadId = existingRecent.thread_id;
// Update thread_id if it's null or in old format edgeLogger.info('Using existing recent submission', {
if (!submissionByTicket.thread_id || submissionByTicket.thread_id !== threadId) { requestId: tracking.requestId,
await supabase submissionId: existingRecent.id,
.from('contact_submissions') ticketNumber: existingRecent.ticket_number
.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 { } else {
submissionError = error2; // Create new contact submission
const { data: newSubmission, error: createError } = await supabase
.from('contact_submissions')
.insert({
name: senderName,
email: senderEmail.toLowerCase(),
subject: subject || '(No Subject)',
message: text || html || '(Empty message)',
category: 'general',
status: 'pending',
user_agent: 'Email Client',
ip_address_hash: null
})
.select('id, ticket_number, email, status')
.single();
if (createError || !newSubmission) {
edgeLogger.error('Failed to create submission from direct email', {
requestId: tracking.requestId,
error: createError
});
return createErrorResponse(createError, 500, corsHeaders);
}
submission = newSubmission;
threadId = `${newSubmission.ticket_number}.${newSubmission.id}`;
// Update thread_id
await supabase
.from('contact_submissions')
.update({ thread_id: threadId })
.eq('id', newSubmission.id);
edgeLogger.info('Created new submission from direct email', {
requestId: tracking.requestId,
submissionId: newSubmission.id,
ticketNumber: newSubmission.ticket_number,
threadId
});
} }
} else { } else {
submissionError = error1; // EXISTING LOGIC: Find submission by thread_id or ticket_number
} const ticketMatch = threadId.match(/(?:ticket-)?(TW-\d+)/i);
const ticketNumber = ticketMatch ? ticketMatch[1] : null;
if (submissionError || !submission) { edgeLogger.info('Thread ID extracted', {
edgeLogger.warn('Submission not found for thread ID', {
requestId: tracking.requestId, requestId: tracking.requestId,
threadId, rawThreadId: threadId,
ticketNumber, 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 // Strategy 1: Try exact thread_id match
const senderEmail = from.match(/<(.+)>/)?.[1] || from; const { data: submissionByThreadId, error: error1 } = await supabase
if (senderEmail.toLowerCase() !== submission.email.toLowerCase()) { .from('contact_submissions')
edgeLogger.warn('Sender email mismatch', { .select('id, email, status, ticket_number')
requestId: tracking.requestId, .eq('thread_id', threadId)
expected: submission.email, .maybeSingle();
received: senderEmail
}); if (submissionByThreadId) {
return new Response(JSON.stringify({ success: false, reason: 'email_mismatch' }), { submission = submissionByThreadId;
status: 200, } else if (ticketNumber) {
headers: { ...corsHeaders, 'Content-Type': 'application/json' } // 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 // Insert email thread record
const senderEmail = from.match(/<(.+)>/)?.[1] || from;
const { error: insertError } = await supabase const { error: insertError } = await supabase
.from('contact_email_threads') .from('contact_email_threads')
.insert({ .insert({
submission_id: submission.id, submission_id: submission.id,
message_id: messageId, message_id: messageId,
in_reply_to: inReplyTo, in_reply_to: inReplyTo || null,
reference_chain: references || [], reference_chain: references || [],
from_email: senderEmail, from_email: senderEmail,
to_email: to, to_email: to,
@@ -157,7 +226,8 @@ const handler = async (req: Request): Promise<Response> => {
direction: 'inbound', direction: 'inbound',
metadata: { metadata: {
received_at: new Date().toISOString(), received_at: new Date().toISOString(),
headers: headers headers: headers,
is_new_ticket: isNewEmail
} }
}); });