fix: propagate SMTP transport errors from EmailService.sendEmail
sendEmail caught transport failures internally and resolved to false or undefined, so every caller's .catch() was dead code and send failures were silently suppressed. Password recovery in particular reported "email sent successfully" while the user received nothing. sendEmail now throws an AppError carrying the underlying cause, and returns Promise<string> instead of Promise<string | false | undefined>. Call sites updated for the new contract: - UserService.registerUser: the existing .catch() now actually fires - UserService.requestRecovery: the failure reaches the client instead of a misleading success response - InviteService and the test-email endpoint: keep their user-facing messages and attach the SMTP cause - EmailProvider: catches and returns false, because NotificationsService fans providers out through Promise.all and a rejection there would abort the other channels' alerts Closes #3757
This commit is contained in:
1 parent
f97ca8565a
commit
c35b22e1ac
9 files changed
+119
-49
No files matched your search
@@ -93,24 +93,30 @@ class SettingsController implements ISettingsController {
|
||||
if (!html) {
|
||||
throw new AppError({ message: "Failed to build email template.", status: 500 });
|
||||
}
|
||||
const messageId = await this.emailService.sendEmail(to, subject, html, {
|
||||
systemEmailHost,
|
||||
systemEmailPort,
|
||||
systemEmailUser,
|
||||
systemEmailAddress,
|
||||
systemEmailDisplayName,
|
||||
systemEmailPassword,
|
||||
systemEmailConnectionHost,
|
||||
systemEmailSecure,
|
||||
systemEmailPool,
|
||||
systemEmailIgnoreTLS,
|
||||
systemEmailRequireTLS,
|
||||
systemEmailRejectUnauthorized,
|
||||
systemEmailTLSServername,
|
||||
});
|
||||
|
||||
if (!messageId) {
|
||||
throw new AppError({ message: "Failed to send test email.", status: 500 });
|
||||
let messageId: string;
|
||||
try {
|
||||
messageId = await this.emailService.sendEmail(to, subject, html, {
|
||||
systemEmailHost,
|
||||
systemEmailPort,
|
||||
systemEmailUser,
|
||||
systemEmailAddress,
|
||||
systemEmailDisplayName,
|
||||
systemEmailPassword,
|
||||
systemEmailConnectionHost,
|
||||
systemEmailSecure,
|
||||
systemEmailPool,
|
||||
systemEmailIgnoreTLS,
|
||||
systemEmailRequireTLS,
|
||||
systemEmailRejectUnauthorized,
|
||||
systemEmailTLSServername,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
// Surface the underlying SMTP failure: diagnosing the settings is the whole
|
||||
// point of the test email endpoint.
|
||||
throw new AppError({
|
||||
message: error instanceof Error ? `Failed to send test email. ${error.message}` : "Failed to send test email.",
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -96,13 +96,15 @@ export class InviteService implements IInviteService {
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.emailService.sendEmail(invite.email, "Welcome to Uptime Monitor", html);
|
||||
if (!result) {
|
||||
try {
|
||||
await this.emailService.sendEmail(invite.email, "Welcome to Uptime Monitor", html);
|
||||
} catch (error: unknown) {
|
||||
throw new AppError({
|
||||
message: "Failed to send invite e-mail... Please verify your settings.",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendInviteEmail",
|
||||
status: 500,
|
||||
details: { cause: error instanceof Error ? error.message : "Unknown error" },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,16 +35,18 @@ export class EmailProvider extends NotificationProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
||||
if (!messageId) {
|
||||
try {
|
||||
await this.emailService.sendEmail(notification.address, subject, html);
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn({
|
||||
message: "Email test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||
@@ -64,16 +66,20 @@ export class EmailProvider extends NotificationProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
||||
if (!messageId) {
|
||||
// Providers must resolve to a boolean: NotificationsService fans these out through
|
||||
// Promise.all, so a rejection here would abort the other channels' alerts too.
|
||||
try {
|
||||
await this.emailService.sendEmail(notification.address, subject, html);
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn({
|
||||
message: "Email notification failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private buildSubject(message: NotificationMessage): string {
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface IUserService {
|
||||
currentUserEmail: string
|
||||
): Promise<User>;
|
||||
checkSuperadminExists(): Promise<boolean>;
|
||||
requestRecovery(email: string): Promise<string | false | undefined>;
|
||||
requestRecovery(email: string): Promise<string>;
|
||||
validateRecovery(recoveryToken: string): Promise<void>;
|
||||
resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>;
|
||||
deleteUser(params: { userId: string; teamId: string; roles: UserRole[] }): Promise<void>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { fileURLToPath } from "url";
|
||||
import { EmailTransportConfig } from "@/domain/app-settings/app-settings.type.js";
|
||||
import { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import nodemailer from "nodemailer";
|
||||
@@ -20,7 +21,7 @@ type TemplateCompiler = (template: string) => (context: Record<string, unknown>)
|
||||
export interface IEmailService {
|
||||
init(): void;
|
||||
buildEmail(template: string, context: Record<string, unknown>): Promise<string | undefined>;
|
||||
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
|
||||
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string>;
|
||||
}
|
||||
|
||||
export class EmailService implements IEmailService {
|
||||
@@ -151,7 +152,12 @@ export class EmailService implements IEmailService {
|
||||
method: "verifyTransporter",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
return false;
|
||||
throw new AppError({
|
||||
message: "Email transporter verification failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendEmail",
|
||||
details: { cause: error instanceof Error ? error.message : "Unknown error" },
|
||||
});
|
||||
}
|
||||
|
||||
const trimmedDisplayName = systemEmailDisplayName?.trim();
|
||||
@@ -172,6 +178,12 @@ export class EmailService implements IEmailService {
|
||||
method: "sendEmail",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
throw new AppError({
|
||||
message: "Failed to send email",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendEmail",
|
||||
details: { cause: error instanceof Error ? error.message : "Unknown error" },
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user