xpeditis2.0/apps/backend/src/infrastructure/email/email.adapter.ts
2025-12-11 15:04:52 +01:00

431 lines
14 KiB
TypeScript

/**
* 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<string>('SMTP_HOST', 'localhost');
const port = this.configService.get<number>('SMTP_PORT', 2525);
const user = this.configService.get<string>('SMTP_USER');
const pass = this.configService.get<string>('SMTP_PASS');
const secure = this.configService.get<boolean>('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<void> {
try {
const from = this.configService.get<string>('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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
// Use APP_URL (frontend) for accept/reject links
// The frontend pages will call the backend API at /accept/:token and /reject/:token
const frontendUrl = this.configService.get('APP_URL', 'http://localhost:3000');
const acceptUrl = `${frontendUrl}/carrier/accept/${bookingData.confirmationToken}`;
const rejectUrl = `${frontendUrl}/carrier/reject/${bookingData.confirmationToken}`;
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<void> {
const baseUrl = this.configService.get('APP_URL', 'http://localhost:3000');
const loginUrl = `${baseUrl}/carrier/login`;
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #0066cc; color: white; padding: 20px; text-align: center; }
.content { padding: 30px; background: #f9f9f9; }
.credentials { background: white; padding: 20px; margin: 20px 0; border-left: 4px solid #0066cc; }
.button { display: inline-block; padding: 12px 30px; background: #0066cc; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0; }
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚢 Bienvenue sur Xpeditis</h1>
</div>
<div class="content">
<h2>Votre compte transporteur a été créé</h2>
<p>Bonjour <strong>${carrierName}</strong>,</p>
<p>Un compte transporteur a été automatiquement créé pour vous sur la plateforme Xpeditis.</p>
<div class="credentials">
<h3>Vos identifiants de connexion :</h3>
<p><strong>Email :</strong> ${email}</p>
<p><strong>Mot de passe temporaire :</strong> <code style="background: #f0f0f0; padding: 5px 10px; border-radius: 3px;">${temporaryPassword}</code></p>
</div>
<p><strong>⚠️ Important :</strong> Pour des raisons de sécurité, nous vous recommandons fortement de changer ce mot de passe temporaire dès votre première connexion.</p>
<div style="text-align: center;">
<a href="${loginUrl}" class="button">Se connecter maintenant</a>
</div>
<h3>Prochaines étapes :</h3>
<ol>
<li>Connectez-vous avec vos identifiants</li>
<li>Changez votre mot de passe</li>
<li>Complétez votre profil transporteur</li>
<li>Consultez vos demandes de réservation</li>
</ol>
</div>
<div class="footer">
<p>© ${new Date().getFullYear()} Xpeditis - Plateforme de fret maritime</p>
<p>Cet email a été envoyé automatiquement, merci de ne pas y répondre.</p>
</div>
</div>
</body>
</html>
`;
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<void> {
const baseUrl = this.configService.get('APP_URL', 'http://localhost:3000');
const loginUrl = `${baseUrl}/carrier/login`;
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #0066cc; color: white; padding: 20px; text-align: center; }
.content { padding: 30px; background: #f9f9f9; }
.credentials { background: white; padding: 20px; margin: 20px 0; border-left: 4px solid #ff9900; }
.button { display: inline-block; padding: 12px 30px; background: #0066cc; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0; }
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
.warning { background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 5px; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔑 Réinitialisation de mot de passe</h1>
</div>
<div class="content">
<h2>Votre mot de passe a été réinitialisé</h2>
<p>Bonjour <strong>${carrierName}</strong>,</p>
<p>Vous avez demandé la réinitialisation de votre mot de passe Xpeditis.</p>
<div class="credentials">
<h3>Votre nouveau mot de passe temporaire :</h3>
<p><code style="background: #f0f0f0; padding: 10px 15px; border-radius: 3px; font-size: 16px; display: inline-block;">${temporaryPassword}</code></p>
</div>
<div class="warning">
<p><strong>⚠️ Sécurité :</strong></p>
<ul style="margin: 10px 0;">
<li>Ce mot de passe est temporaire et doit être changé immédiatement</li>
<li>Ne partagez jamais vos identifiants avec qui que ce soit</li>
<li>Si vous n'avez pas demandé cette réinitialisation, contactez-nous immédiatement</li>
</ul>
</div>
<div style="text-align: center;">
<a href="${loginUrl}" class="button">Se connecter et changer le mot de passe</a>
</div>
<p style="margin-top: 30px;">Si vous rencontrez des difficultés, n'hésitez pas à contacter notre équipe support.</p>
</div>
<div class="footer">
<p>© ${new Date().getFullYear()} Xpeditis - Plateforme de fret maritime</p>
<p>Cet email a été envoyé automatiquement, merci de ne pas y répondre.</p>
</div>
</div>
</body>
</html>
`;
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}`);
}
}