mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:31:12 -05:00
Refactor: Handle direct emails to admin
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user