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 AdminReplyRequest { submissionId: string; replyBody: string; replySubject?: string; } const handler = async (req: Request): Promise => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } const tracking = startRequest(); try { const authHeader = req.headers.get('Authorization'); if (!authHeader) { return createErrorResponse({ message: 'Unauthorized' }, 401, corsHeaders); } const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!, { global: { headers: { Authorization: authHeader } } } ); const { data: { user }, error: userError } = await supabase.auth.getUser(); if (userError || !user) { return createErrorResponse({ message: 'Unauthorized' }, 401, corsHeaders); } // Verify admin, moderator, or superuser role const { data: isSuperuser } = await supabase .rpc('has_role', { _user_id: user.id, _role: 'superuser' }); const { data: isAdmin } = await supabase .rpc('has_role', { _user_id: user.id, _role: 'admin' }); const { data: isModerator } = await supabase .rpc('has_role', { _user_id: user.id, _role: 'moderator' }); if (!isSuperuser && !isAdmin && !isModerator) { edgeLogger.warn('Non-privileged user attempted email reply', { requestId: tracking.requestId, userId: user.id }); return createErrorResponse({ message: 'Admin access required' }, 403, corsHeaders); } const body: AdminReplyRequest = await req.json(); const { submissionId, replyBody, replySubject } = body; if (!submissionId || !replyBody) { return createErrorResponse({ message: 'Missing required fields' }, 400, corsHeaders); } if (replyBody.length < 10 || replyBody.length > 5000) { return createErrorResponse({ message: 'Reply must be between 10 and 5000 characters' }, 400, corsHeaders); } // 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) { return createErrorResponse({ message: 'Submission not found' }, 404, corsHeaders); } // 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) { return createErrorResponse({ message: 'Rate limit exceeded. Max 10 replies per hour.' }, 429, corsHeaders); } 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; // 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(); edgeLogger.error('ForwardEmail API error', { requestId: tracking.requestId, status: forwardEmailResponse.status, error: errorText }); return createErrorResponse({ message: 'Failed to send email' }, 500, corsHeaders); } // 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) { edgeLogger.error('Failed to insert email thread', { requestId: tracking.requestId, error: insertError }); } // 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); // 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 } }); edgeLogger.info('Admin email reply sent successfully', { requestId: tracking.requestId, submissionId, duration: endRequest(tracking) }); return new Response( JSON.stringify({ success: true, messageId }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } catch (error) { edgeLogger.error('Unexpected error in send-admin-email-reply', { requestId: tracking.requestId, error: error instanceof Error ? error.message : String(error) }); return createErrorResponse(error, 500, corsHeaders); } }; serve(handler);