Merge pull request #3837 from egeoztass/fix/email-error-propagation

fix: propagate SMTP transport errors from EmailService.sendEmail
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-08-18 10:36:02 -07:00
commit 6eba53d716
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({
+4 -2
View File
@@ -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 {
+1 -1
View File
@@ -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>;
+14 -2
View File
@@ -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" },
});
}
};
}