459 lines
No EOL
12 KiB
TypeScript
459 lines
No EOL
12 KiB
TypeScript
// 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<string, any>;
|
|
|
|
// 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<string, any>;
|
|
context?: Record<string, any>;
|
|
|
|
// 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>" -> "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<string | null> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<number> {
|
|
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,
|
|
};
|
|
} |