import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { corsHeaders } from '../_shared/cors.ts'; import { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts'; import { addSpanEvent } from '../_shared/logger.ts'; interface AdminReplyRequest { submissionId: string; replyBody: string; replySubject?: string; } const handler = async (req: Request, { supabase, user, span, requestId }: EdgeFunctionContext): Promise => { const body: AdminReplyRequest = await req.json(); const { submissionId, replyBody, replySubject } = body; // Validate request if (!submissionId || !replyBody) { addSpanEvent(span, 'validation_failed', { reason: 'missing_fields' }); return new Response( JSON.stringify({ error: 'Missing required fields', requestId }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } if (replyBody.length < 10 || replyBody.length > 5000) { addSpanEvent(span, 'validation_failed', { reason: 'invalid_length', length: replyBody.length }); return new Response( JSON.stringify({ error: 'Reply must be between 10 and 5000 characters', requestId }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } addSpanEvent(span, 'request_validated', { submissionId }); // Get admin email from environment variable const adminEmail = Deno.env.get('ADMIN_EMAIL_ADDRESS') || 'admins@thrillwiki.com'; const adminDisplayName = Deno.env.get('ADMIN_EMAIL_DISPLAY_NAME') || 'ThrillWiki Admin'; // Fetch admin's profile for signature const { data: adminProfile } = await supabase .from('profiles') .select('display_name, username') .eq('user_id', user.id) .single(); const adminName = adminProfile?.display_name || adminProfile?.username || 'ThrillWiki Team'; // Fetch submission const { data: submission, error: fetchError } = await supabase .from('contact_submissions') .select('id, email, name, subject, thread_id, response_count, ticket_number') .eq('id', submissionId) .single(); if (fetchError || !submission) { addSpanEvent(span, 'submission_not_found', { submissionId }); return new Response( JSON.stringify({ error: 'Submission not found', requestId }), { status: 404, headers: { 'Content-Type': 'application/json' } } ); } addSpanEvent(span, 'submission_fetched', { ticketNumber: submission.ticket_number, recipientEmail: submission.email }); // Fetch email signature from admin settings const { data: signatureSetting } = await supabase .from('admin_settings') .select('setting_value') .eq('setting_key', 'email.signature') .single(); const emailSignature = signatureSetting?.setting_value?.signature || ''; // Build signature with admin name + global signature const finalReplyBody = emailSignature ? `${replyBody}\n\n---\n${adminName}\n${emailSignature}` : `${replyBody}\n\n---\n${adminName}`; // Rate limiting: max 10 replies per hour const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString(); const { count } = await supabase .from('contact_email_threads') .select('*', { count: 'exact', head: true }) .eq('submission_id', submissionId) .eq('direction', 'outbound') .gte('created_at', oneHourAgo); if (count && count >= 10) { addSpanEvent(span, 'rate_limit_exceeded', { count }); return new Response( JSON.stringify({ error: 'Rate limit exceeded. Max 10 replies per hour.', requestId }), { status: 429, headers: { 'Content-Type': 'application/json' } } ); } const ticketNumber = submission.ticket_number || 'UNKNOWN'; const messageId = `<${ticketNumber}.${crypto.randomUUID()}@thrillwiki.com>`; const finalSubject = replySubject || `Re: [${ticketNumber}] ${submission.subject}`; // Get previous message for threading const { data: previousMessages } = await supabase .from('contact_email_threads') .select('message_id') .eq('submission_id', submissionId) .order('created_at', { ascending: false }) .limit(1); const originalMessageId = `<${ticketNumber}.${submission.id}@thrillwiki.com>`; const inReplyTo = previousMessages?.[0]?.message_id || originalMessageId; // Build reference chain for threading const referenceChain = previousMessages?.[0]?.message_id ? [originalMessageId, previousMessages[0].message_id].join(' ') : originalMessageId; addSpanEvent(span, 'sending_email', { messageId, recipient: submission.email, subject: finalSubject }); // Send email via ForwardEmail const forwardEmailResponse = await fetch('https://api.forwardemail.net/v1/emails', { method: 'POST', headers: { 'Authorization': `Basic ${btoa(Deno.env.get('FORWARDEMAIL_API_KEY') + ':')}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ from: `${adminDisplayName} <${adminEmail}>`, to: `${submission.name} <${submission.email}>`, subject: finalSubject, text: finalReplyBody, headers: { 'Message-ID': messageId, 'In-Reply-To': inReplyTo, 'References': referenceChain, 'X-Thread-ID': submission.thread_id, 'X-Ticket-Number': ticketNumber } }) }); if (!forwardEmailResponse.ok) { const errorText = await forwardEmailResponse.text(); addSpanEvent(span, 'email_send_failed', { status: forwardEmailResponse.status, error: errorText }); throw new Error(`ForwardEmail API error: ${errorText}`); } addSpanEvent(span, 'email_sent', { messageId }); // Insert email thread record const { error: insertError } = await supabase .from('contact_email_threads') .insert({ submission_id: submissionId, message_id: messageId, in_reply_to: inReplyTo, reference_chain: [inReplyTo], from_email: adminEmail, to_email: submission.email, subject: finalSubject, body_text: finalReplyBody, direction: 'outbound', sent_by: user.id, metadata: { admin_email: user.email, sent_at: new Date().toISOString() } }); if (insertError) { addSpanEvent(span, 'thread_insert_failed', { error: insertError }); } else { addSpanEvent(span, 'thread_inserted'); } // Update submission await supabase .from('contact_submissions') .update({ last_admin_response_at: new Date().toISOString(), response_count: (submission.response_count || 0) + 1, status: 'in_progress' }) .eq('id', submissionId); addSpanEvent(span, 'submission_updated'); // Audit log await supabase .from('admin_audit_log') .insert({ admin_user_id: user.id, target_user_id: user.id, action: 'send_contact_email_reply', details: { submission_id: submissionId, recipient: submission.email, subject: finalSubject } }); addSpanEvent(span, 'audit_logged'); return new Response( JSON.stringify({ success: true, messageId }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); }; serve(createEdgeFunction({ name: 'send-admin-email-reply', requireAuth: true, requiredRoles: ['superuser', 'admin', 'moderator'], corsHeaders, logRequests: true, logResponses: true, }, handler));