fix(mail): retry SMTP transient + erreur API si email refus échoue

- Transporteur SMTP réutilisé (pool) au lieu d'une auth par email
- 3 tentatives avec backoff sur erreurs 454/timeout/coupure Dovecot
- refuseUser renvoie 503 si un email de refus échoue après retry
- Tests unitaires isTransientSmtpError

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-15 22:50:53 +02:00
co-authored by Cursor
parent f71943fa79
commit e1684676a7
3 changed files with 167 additions and 42 deletions
+10 -2
View File
@@ -1,4 +1,4 @@
import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException, ServiceUnavailableException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { RoleType, StatutUtilisateurType, Users } from "src/entities/users.entity";
import { In, MoreThan, Repository } from "typeorm";
@@ -390,6 +390,7 @@ export class UserService {
return updatedUsers;
});
const emailFailures: string[] = [];
for (const savedUser of savedUsers) {
try {
await this.mailService.sendRefusEmail(
@@ -400,10 +401,17 @@ export class UserService {
tokenReprise,
);
} catch (err) {
this.logger.warn(`Envoi email refus échoué pour ${savedUser.email}`, err);
emailFailures.push(savedUser.email);
this.logger.error(`Envoi email refus échoué pour ${savedUser.email}`, err);
}
}
if (emailFailures.length > 0) {
throw new ServiceUnavailableException(
`Le dossier a bien été refusé en base, mais l'envoi d'email a échoué pour : ${emailFailures.join(', ')}. Réessayez ou contactez le support.`,
);
}
return savedUsers.find((savedUser) => savedUser.id === user.id) ?? savedUsers[0];
}