// lib/emailLoggingService.ts import { createSbServiceRole } from "@/lib/supabaseServer"; export type EmailType = | 'contract-created' | 'contract-updated' | 'contract-cancelled' | 'employee-created' | 'invitation' | 'auto-declaration-invitation' | 'invoice' | 'signature-request' | 'signature-request-employer' | 'signature-request-employee' | 'signature-request-employee-amendment' // Signature avenant par salarié | 'signature-request-salarie' // Demande signature salarié (depuis Lambda DocuSeal) | 'bulk-signature-notification' | 'salary-transfer-notification' | 'salary-transfer-payment-confirmation' // Confirmation paiement salaires effectué | 'contribution-notification' // Notification de cotisations | 'notification' | 'support-reply' // Réponse support | 'support-ticket-created' // Nouveau ticket | 'support-ticket-reply' // Réponse utilisateur | 'contact-support' // Formulaire contact public | 'amendment-completed-employer' // Avenant signé (notification employeur) | 'amendment-completed-employee' // Avenant signé (notification salarié) | 'referral' // Email d'invitation au parrainage | 'account-activation' | 'access-updated' | 'access-revoked' | 'password-created' | 'password-changed' | 'twofa-enabled' | 'twofa-disabled' | 'bulk-email' | 'bulk_communication' | 'system-notification' | 'other'; export type EmailStatus = | 'pending' | 'sending' | 'sent' | 'delivered' | 'bounce' | 'complaint' | 'failed'; export interface EmailLogData { // Informations obligatoires senderEmail: string; recipientEmail: string; subject: string; emailType: EmailType; // Contenu htmlContent?: string; textContent?: string; templateName?: string; templateData?: Record; // Destinataires additionnels ccEmails?: string[]; bccEmails?: string[]; // Informations utilisateur senderUserId?: string; senderName?: string; // SES sesMessageId?: string; sesRegion?: string; sesConfigurationSet?: string; sesSourceArn?: string; sesReturnPath?: string; // Contexte organizationId?: string; contractId?: string; ticketId?: string; userAgent?: string; ipAddress?: string; // Métadonnées tags?: Record; context?: Record; // Statut emailStatus?: EmailStatus; } export interface EmailLogUpdate { emailStatus?: EmailStatus; sesMessageId?: string; bounceReason?: string; complaintReason?: string; failureReason?: string; sentAt?: Date; deliveredAt?: Date; } class EmailLoggingService { private sb = createSbServiceRole(); /** * Extraire l'adresse email pure d'une chaîne qui peut contenir un nom d'affichage * Ex: "Odentas " -> "paie@odentas.fr" */ private extractPureEmail(emailString: string): string { // Rechercher un email entre < > const match = emailString.match(/<([^>]+)>/); if (match) { return match[1].trim(); } // Si pas de < >, retourner la chaîne nettoyée return emailString.trim().replace(/['"]/g, ''); } /** * Créer un nouveau log d'email */ async logEmail(data: EmailLogData): Promise { try { const cleanedSenderEmail = this.extractPureEmail(data.senderEmail); const cleanedRecipientEmail = this.extractPureEmail(data.recipientEmail); const logEntry = { sender_email: cleanedSenderEmail, recipient_email: cleanedRecipientEmail, cc_emails: data.ccEmails?.map(email => this.extractPureEmail(email)) || null, bcc_emails: data.bccEmails || null, subject: data.subject, html_content: data.htmlContent || null, text_content: data.textContent || null, template_name: data.templateName || null, template_data: data.templateData || null, email_type: data.emailType, email_status: data.emailStatus || 'pending', // Informations utilisateur sender_user_id: data.senderUserId || null, sender_name: data.senderName || null, // SES ses_message_id: data.sesMessageId || null, ses_region: data.sesRegion || process.env.AWS_REGION || 'eu-west-3', ses_configuration_set: data.sesConfigurationSet || null, ses_source_arn: data.sesSourceArn || null, ses_return_path: data.sesReturnPath || null, // Contexte organization_id: data.organizationId || null, contract_id: data.contractId || null, ticket_id: data.ticketId || null, user_agent: data.userAgent || null, ip_address: data.ipAddress || null, // Métadonnées tags: data.tags || {}, context: data.context || {}, created_at: new Date().toISOString() }; const { data: result, error } = await this.sb .from('email_logs') .insert(logEntry) .select('id') .single(); if (error) { console.error('Erreur lors de la création du log email:', error); return null; } return result?.id || null; } catch (error) { console.error('Exception lors de la création du log email:', error); return null; } } /** * Mettre à jour un log d'email existant */ async updateEmailLog(logId: string, updates: EmailLogUpdate): Promise { try { const updateData: any = {}; if (updates.emailStatus) { updateData.email_status = updates.emailStatus; } if (updates.sesMessageId) { updateData.ses_message_id = updates.sesMessageId; } if (updates.bounceReason) { updateData.bounce_reason = updates.bounceReason; } if (updates.complaintReason) { updateData.complaint_reason = updates.complaintReason; } if (updates.failureReason) { updateData.failure_reason = updates.failureReason; } if (updates.sentAt) { updateData.sent_at = updates.sentAt.toISOString(); } if (updates.deliveredAt) { updateData.delivered_at = updates.deliveredAt.toISOString(); } const { error } = await this.sb .from('email_logs') .update(updateData) .eq('id', logId); if (error) { console.error('Erreur lors de la mise à jour du log email:', error); return false; } return true; } catch (error) { console.error('Exception lors de la mise à jour du log email:', error); return false; } } /** * Mettre à jour un log par SES Message ID */ async updateEmailLogBySesId(sesMessageId: string, updates: EmailLogUpdate): Promise { try { const updateData: any = {}; if (updates.emailStatus) { updateData.email_status = updates.emailStatus; } if (updates.bounceReason) { updateData.bounce_reason = updates.bounceReason; } if (updates.complaintReason) { updateData.complaint_reason = updates.complaintReason; } if (updates.failureReason) { updateData.failure_reason = updates.failureReason; } if (updates.sentAt) { updateData.sent_at = updates.sentAt.toISOString(); } if (updates.deliveredAt) { updateData.delivered_at = updates.deliveredAt.toISOString(); } const { error } = await this.sb .from('email_logs') .update(updateData) .eq('ses_message_id', sesMessageId); if (error) { console.error('Erreur lors de la mise à jour du log email par SES ID:', error); return false; } return true; } catch (error) { console.error('Exception lors de la mise à jour du log email par SES ID:', error); return false; } } /** * Récupérer les logs d'email avec filtres */ async getEmailLogs(options: { limit?: number; offset?: number; emailType?: EmailType; emailStatus?: EmailStatus; senderEmail?: string; recipientEmail?: string; organizationId?: string; dateFrom?: Date; dateTo?: Date; searchTerm?: string; } = {}) { try { let query = this.sb .from('email_logs') .select(` id, created_at, sent_at, delivered_at, sender_email, sender_name, recipient_email, cc_emails, bcc_emails, subject, email_type, email_status, ses_message_id, organization_id, contract_id, ticket_id, bounce_reason, complaint_reason, failure_reason, tags, context `); // Filtres if (options.emailType) { query = query.eq('email_type', options.emailType); } if (options.emailStatus) { query = query.eq('email_status', options.emailStatus); } if (options.senderEmail) { query = query.eq('sender_email', options.senderEmail); } if (options.recipientEmail) { query = query.eq('recipient_email', options.recipientEmail); } if (options.organizationId) { query = query.eq('organization_id', options.organizationId); } if (options.dateFrom) { query = query.gte('created_at', options.dateFrom.toISOString()); } if (options.dateTo) { query = query.lte('created_at', options.dateTo.toISOString()); } if (options.searchTerm) { query = query.or(`subject.ilike.%${options.searchTerm}%,recipient_email.ilike.%${options.searchTerm}%,sender_email.ilike.%${options.searchTerm}%`); } // Pagination et tri query = query .order('created_at', { ascending: false }) .range(options.offset || 0, (options.offset || 0) + (options.limit || 50) - 1); const { data, error } = await query; if (error) { console.error('Erreur lors de la récupération des logs email:', error); return { data: [], error }; } return { data: data || [], error: null }; } catch (error) { console.error('Exception lors de la récupération des logs email:', error); return { data: [], error }; } } /** * Récupérer les statistiques d'email */ async getEmailStats(options: { dateFrom?: Date; dateTo?: Date; organizationId?: string; } = {}) { try { let query = this.sb .from('email_stats_summary') .select('*'); if (options.dateFrom) { query = query.gte('date', options.dateFrom.toISOString().split('T')[0]); } if (options.dateTo) { query = query.lte('date', options.dateTo.toISOString().split('T')[0]); } const { data, error } = await query; if (error) { console.error('Erreur lors de la récupération des stats email:', error); return { data: [], error }; } return { data: data || [], error: null }; } catch (error) { console.error('Exception lors de la récupération des stats email:', error); return { data: [], error }; } } /** * Nettoyer les anciens logs (à exécuter périodiquement) */ async cleanupOldLogs(daysToKeep: number = 90): Promise { try { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); const { data, error } = await this.sb .from('email_logs') .delete() .lt('created_at', cutoffDate.toISOString()) .select('id'); if (error) { console.error('Erreur lors du nettoyage des logs email:', error); return 0; } return data?.length || 0; } catch (error) { console.error('Exception lors du nettoyage des logs email:', error); return 0; } } } // Instance singleton export const emailLogger = new EmailLoggingService(); // Helpers pour extraction de données SES export function extractSesDataFromResponse(sesResponse: any) { return { sesMessageId: sesResponse.MessageId, sesRegion: process.env.AWS_REGION || 'eu-west-3', }; } export function extractUserContextFromRequest(request: any) { return { userAgent: request.headers.get('user-agent') || undefined, ipAddress: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || undefined, }; }