/** * Email Adapter * * Implements EmailPort using nodemailer */ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as nodemailer from 'nodemailer'; import { EmailPort, EmailOptions } from '@domain/ports/out/email.port'; import { EmailTemplates } from './templates/email-templates'; @Injectable() export class EmailAdapter implements EmailPort { private readonly logger = new Logger(EmailAdapter.name); private transporter: nodemailer.Transporter; constructor( private readonly configService: ConfigService, private readonly emailTemplates: EmailTemplates ) { this.initializeTransporter(); } private initializeTransporter(): void { const host = this.configService.get('SMTP_HOST', 'localhost'); const port = this.configService.get('SMTP_PORT', 2525); const user = this.configService.get('SMTP_USER'); const pass = this.configService.get('SMTP_PASS'); const secure = this.configService.get('SMTP_SECURE', false); // 🔧 FIX: Contournement DNS pour Mailtrap // Utilise automatiquement l'IP directe quand 'mailtrap.io' est détecté // Cela évite les timeouts DNS (queryA ETIMEOUT) sur certains réseaux const useDirectIP = host.includes('mailtrap.io'); const actualHost = useDirectIP ? '3.209.246.195' : host; const serverName = useDirectIP ? 'smtp.mailtrap.io' : host; // Pour TLS this.transporter = nodemailer.createTransport({ host: actualHost, port, secure, auth: { user, pass, }, // Configuration TLS avec servername pour IP directe tls: { rejectUnauthorized: false, servername: serverName, // ⚠️ CRITIQUE pour TLS avec IP directe }, // Timeouts optimisés connectionTimeout: 10000, // 10s greetingTimeout: 10000, // 10s socketTimeout: 30000, // 30s dnsTimeout: 10000, // 10s }); this.logger.log( `Email adapter initialized with SMTP host: ${host}:${port} (secure: ${secure})` + (useDirectIP ? ` [Using direct IP: ${actualHost} with servername: ${serverName}]` : '') ); } async send(options: EmailOptions): Promise { try { const from = this.configService.get('SMTP_FROM', 'noreply@xpeditis.com'); await this.transporter.sendMail({ from, to: options.to, cc: options.cc, bcc: options.bcc, replyTo: options.replyTo, subject: options.subject, html: options.html, text: options.text, attachments: options.attachments, }); this.logger.log(`Email sent to ${options.to}: ${options.subject}`); } catch (error) { this.logger.error(`Failed to send email to ${options.to}`, error); throw error; } } async sendBookingConfirmation( email: string, bookingNumber: string, bookingDetails: any, pdfAttachment?: Buffer ): Promise { const html = await this.emailTemplates.renderBookingConfirmation({ bookingNumber, bookingDetails, }); const attachments = pdfAttachment ? [ { filename: `booking-${bookingNumber}.pdf`, content: pdfAttachment, contentType: 'application/pdf', }, ] : undefined; await this.send({ to: email, subject: `Booking Confirmation - ${bookingNumber}`, html, attachments, }); } async sendVerificationEmail(email: string, token: string): Promise { const verifyUrl = `${this.configService.get('APP_URL')}/verify-email?token=${token}`; const html = await this.emailTemplates.renderVerificationEmail({ verifyUrl, }); await this.send({ to: email, subject: 'Verify your email - Xpeditis', html, }); } async sendPasswordResetEmail(email: string, token: string): Promise { const resetUrl = `${this.configService.get('APP_URL')}/reset-password?token=${token}`; const html = await this.emailTemplates.renderPasswordResetEmail({ resetUrl, }); await this.send({ to: email, subject: 'Reset your password - Xpeditis', html, }); } async sendWelcomeEmail(email: string, firstName: string): Promise { const html = await this.emailTemplates.renderWelcomeEmail({ firstName, dashboardUrl: `${this.configService.get('APP_URL')}/dashboard`, }); await this.send({ to: email, subject: 'Welcome to Xpeditis', html, }); } async sendUserInvitation( email: string, organizationName: string, inviterName: string, tempPassword: string ): Promise { const loginUrl = `${this.configService.get('APP_URL')}/login`; const html = await this.emailTemplates.renderUserInvitation({ organizationName, inviterName, tempPassword, loginUrl, }); await this.send({ to: email, subject: `You've been invited to join ${organizationName} on Xpeditis`, html, }); } async sendInvitationWithToken( email: string, firstName: string, lastName: string, organizationName: string, inviterName: string, invitationLink: string, expiresAt: Date ): Promise { try { this.logger.log(`[sendInvitationWithToken] Starting email generation for ${email}`); const expiresAtFormatted = expiresAt.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', }); this.logger.log(`[sendInvitationWithToken] Rendering template...`); const html = await this.emailTemplates.renderInvitationWithToken({ firstName, lastName, organizationName, inviterName, invitationLink, expiresAt: expiresAtFormatted, }); this.logger.log(`[sendInvitationWithToken] Template rendered, sending email to ${email}...`); this.logger.log(`[sendInvitationWithToken] HTML size: ${html.length} bytes`); await this.send({ to: email, subject: `Invitation à rejoindre ${organizationName} sur Xpeditis`, html, }); this.logger.log(`Invitation email sent to ${email} for ${organizationName}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorCode = (error as any).code; const errorResponse = (error as any).response; const errorResponseCode = (error as any).responseCode; const errorCommand = (error as any).command; this.logger.error(`[sendInvitationWithToken] ERROR MESSAGE: ${errorMessage}`); this.logger.error(`[sendInvitationWithToken] ERROR CODE: ${errorCode}`); this.logger.error(`[sendInvitationWithToken] ERROR RESPONSE: ${errorResponse}`); this.logger.error(`[sendInvitationWithToken] ERROR RESPONSE CODE: ${errorResponseCode}`); this.logger.error(`[sendInvitationWithToken] ERROR COMMAND: ${errorCommand}`); if (error instanceof Error && error.stack) { this.logger.error(`[sendInvitationWithToken] STACK: ${error.stack.substring(0, 500)}`); } throw error; } } async sendCsvBookingRequest( carrierEmail: string, bookingData: { bookingId: string; origin: string; destination: string; volumeCBM: number; weightKG: number; palletCount: number; priceUSD: number; priceEUR: number; primaryCurrency: string; transitDays: number; containerType: string; documents: Array<{ type: string; fileName: string; }>; confirmationToken: string; } ): Promise { const baseUrl = this.configService.get('APP_URL', 'http://localhost:3000'); const acceptUrl = `${baseUrl}/api/v1/csv-bookings/${bookingData.confirmationToken}/accept`; const rejectUrl = `${baseUrl}/api/v1/csv-bookings/${bookingData.confirmationToken}/reject`; const html = await this.emailTemplates.renderCsvBookingRequest({ ...bookingData, acceptUrl, rejectUrl, }); await this.send({ to: carrierEmail, subject: `Nouvelle demande de réservation - ${bookingData.origin} → ${bookingData.destination}`, html, }); this.logger.log( `CSV booking request sent to ${carrierEmail} for booking ${bookingData.bookingId}` ); } /** * Send carrier account creation email with temporary password */ async sendCarrierAccountCreated( email: string, carrierName: string, temporaryPassword: string ): Promise { const baseUrl = this.configService.get('APP_URL', 'http://localhost:3000'); const loginUrl = `${baseUrl}/carrier/login`; const html = `

🚢 Bienvenue sur Xpeditis

Votre compte transporteur a été créé

Bonjour ${carrierName},

Un compte transporteur a été automatiquement créé pour vous sur la plateforme Xpeditis.

Vos identifiants de connexion :

Email : ${email}

Mot de passe temporaire : ${temporaryPassword}

⚠️ Important : Pour des raisons de sécurité, nous vous recommandons fortement de changer ce mot de passe temporaire dès votre première connexion.

Prochaines étapes :

  1. Connectez-vous avec vos identifiants
  2. Changez votre mot de passe
  3. Complétez votre profil transporteur
  4. Consultez vos demandes de réservation
`; await this.send({ to: email, subject: '🚢 Votre compte transporteur Xpeditis a été créé', html, }); this.logger.log(`Carrier account creation email sent to ${email}`); } /** * Send carrier password reset email with temporary password */ async sendCarrierPasswordReset( email: string, carrierName: string, temporaryPassword: string ): Promise { const baseUrl = this.configService.get('APP_URL', 'http://localhost:3000'); const loginUrl = `${baseUrl}/carrier/login`; const html = `

🔑 Réinitialisation de mot de passe

Votre mot de passe a été réinitialisé

Bonjour ${carrierName},

Vous avez demandé la réinitialisation de votre mot de passe Xpeditis.

Votre nouveau mot de passe temporaire :

${temporaryPassword}

⚠️ Sécurité :

  • Ce mot de passe est temporaire et doit être changé immédiatement
  • Ne partagez jamais vos identifiants avec qui que ce soit
  • Si vous n'avez pas demandé cette réinitialisation, contactez-nous immédiatement

Si vous rencontrez des difficultés, n'hésitez pas à contacter notre équipe support.

`; await this.send({ to: email, subject: '🔑 Réinitialisation de votre mot de passe Xpeditis', html, }); this.logger.log(`Carrier password reset email sent to ${email}`); } }