diff --git a/client/src/Hooks/useSettingsForm.ts b/client/src/Hooks/useSettingsForm.ts
index 4b0768d79..643e9caca 100644
--- a/client/src/Hooks/useSettingsForm.ts
+++ b/client/src/Hooks/useSettingsForm.ts
@@ -18,6 +18,7 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
systemEmailHost: data?.systemEmailHost || "",
systemEmailUser: data?.systemEmailUser || "",
systemEmailAddress: data?.systemEmailAddress || "",
+ systemEmailDisplayName: data?.systemEmailDisplayName || "",
systemEmailConnectionHost: data?.systemEmailConnectionHost || "localhost",
systemEmailTLSServername: data?.systemEmailTLSServername || "",
systemEmailPort: data?.systemEmailPort,
diff --git a/client/src/Pages/Settings/index.tsx b/client/src/Pages/Settings/index.tsx
index 3c6e8a3a1..e14366e0f 100644
--- a/client/src/Pages/Settings/index.tsx
+++ b/client/src/Pages/Settings/index.tsx
@@ -182,6 +182,9 @@ export const SettingsPage = () => {
systemEmailRequireTLS: formValues.systemEmailRequireTLS,
systemEmailRejectUnauthorized: formValues.systemEmailRejectUnauthorized,
...(formValues.systemEmailUser && { systemEmailUser: formValues.systemEmailUser }),
+ ...(formValues.systemEmailDisplayName && {
+ systemEmailDisplayName: formValues.systemEmailDisplayName,
+ }),
...(formValues.systemEmailTLSServername && {
systemEmailTLSServername: formValues.systemEmailTLSServername,
}),
@@ -577,44 +580,65 @@ export const SettingsPage = () => {
href="https://nodemailer.com/smtp/"
target="_blank"
/>
-
-
- {JSON.stringify(
- {
- host: form.watch("systemEmailHost") || "",
- port: form.watch("systemEmailPort") || "",
- secure: form.watch("systemEmailSecure") ?? false,
- auth: {
- user:
- form.watch("systemEmailUser") ||
- form.watch("systemEmailAddress") ||
- "",
- pass: "",
- },
- name: form.watch("systemEmailConnectionHost") || "localhost",
- pool: form.watch("systemEmailPool") ?? false,
- tls: {
- rejectUnauthorized:
- form.watch("systemEmailRejectUnauthorized") ?? true,
- ignoreTLS: form.watch("systemEmailIgnoreTLS") ?? false,
- requireTLS: form.watch("systemEmailRequireTLS") ?? false,
- servername: form.watch("systemEmailTLSServername") || "",
- },
- },
- null,
- 2
- )}
-
-
+ {(() => {
+ const address = form.watch("systemEmailAddress") || "";
+ const displayName = form.watch("systemEmailDisplayName")?.trim();
+ return (
+ <>
+
+
+ {JSON.stringify(
+ {
+ host: form.watch("systemEmailHost") || "",
+ port: form.watch("systemEmailPort") || "",
+ secure: form.watch("systemEmailSecure") ?? false,
+ auth: {
+ user: form.watch("systemEmailUser") || address,
+ pass: "",
+ },
+ name: form.watch("systemEmailConnectionHost") || "localhost",
+ pool: form.watch("systemEmailPool") ?? false,
+ tls: {
+ rejectUnauthorized:
+ form.watch("systemEmailRejectUnauthorized") ?? true,
+ ignoreTLS: form.watch("systemEmailIgnoreTLS") ?? false,
+ requireTLS: form.watch("systemEmailRequireTLS") ?? false,
+ servername: form.watch("systemEmailTLSServername") || "",
+ },
+ },
+ null,
+ 2
+ )}
+
+
+ {address && (
+
+
+ {`From: ${displayName ? `"${displayName}" <${address}>` : address}`}
+
+
+ )}
+ >
+ );
+ })()}
}
rightContent={
@@ -680,6 +704,24 @@ export const SettingsPage = () => {
)}
/>
+ {/* Email Display Name (Optional) */}
+ (
+
+ )}
+ />
+
{/* Email User (Optional) */}
(val === "" ? null : val.toLowerCase().trim()))
.optional(),
+ systemEmailDisplayName: z
+ .string()
+ .max(100, "Display name must be 100 characters or fewer")
+ .transform((val) => (val.trim() === "" ? null : val.trim()))
+ .optional(),
systemEmailUser: z
.string()
.transform((val) => (val.trim() === "" ? null : val.trim()))
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 3c632ef96..d6e8bb4f3 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -1198,6 +1198,10 @@
"label": "Email address - Used for authentication",
"placeholder": "you@example.com"
},
+ "displayName": {
+ "label": "Sender name (optional) - Friendly name shown in the From field",
+ "placeholder": "Checkmate notifications"
+ },
"user": {
"label": "Email user - Username for authentication, overrides email address if specified",
"placeholder": "Leave empty if not required"
diff --git a/server/src/controllers/settingsController.ts b/server/src/controllers/settingsController.ts
index 03bc91898..485a90d64 100644
--- a/server/src/controllers/settingsController.ts
+++ b/server/src/controllers/settingsController.ts
@@ -89,6 +89,7 @@ class SettingsController implements ISettingsController {
systemEmailHost,
systemEmailPort,
systemEmailAddress,
+ systemEmailDisplayName,
systemEmailPassword,
systemEmailUser,
systemEmailConnectionHost,
@@ -112,6 +113,7 @@ class SettingsController implements ISettingsController {
systemEmailPort,
systemEmailUser,
systemEmailAddress,
+ systemEmailDisplayName,
systemEmailPassword,
systemEmailConnectionHost,
systemEmailSecure,
diff --git a/server/src/db/models/AppSettings.ts b/server/src/db/models/AppSettings.ts
index 9eeaa640a..195f43044 100644
--- a/server/src/db/models/AppSettings.ts
+++ b/server/src/db/models/AppSettings.ts
@@ -26,6 +26,7 @@ const AppSettingsSchema = new Schema(
systemEmailHost: { type: String },
systemEmailPort: { type: Number },
systemEmailAddress: { type: String },
+ systemEmailDisplayName: { type: String },
systemEmailPassword: { type: String },
systemEmailUser: { type: String },
systemEmailConnectionHost: { type: String, default: "localhost" },
diff --git a/server/src/service/infrastructure/emailService.ts b/server/src/service/infrastructure/emailService.ts
index 6cf8caf4f..4428b0477 100755
--- a/server/src/service/infrastructure/emailService.ts
+++ b/server/src/service/infrastructure/emailService.ts
@@ -118,6 +118,7 @@ export class EmailService implements IEmailService {
systemEmailPool,
systemEmailUser,
systemEmailAddress,
+ systemEmailDisplayName,
systemEmailPassword,
systemEmailConnectionHost,
systemEmailTLSServername,
@@ -158,10 +159,13 @@ export class EmailService implements IEmailService {
return false;
}
+ const trimmedDisplayName = systemEmailDisplayName?.trim();
+ const from = trimmedDisplayName && systemEmailAddress ? { name: trimmedDisplayName, address: systemEmailAddress } : systemEmailAddress;
+
try {
const info = await this.transporter.sendMail({
to: to,
- from: systemEmailAddress,
+ from: from,
subject: subject,
html: html,
});
diff --git a/server/src/types/email.ts b/server/src/types/email.ts
index 2ffae187b..6e884319f 100644
--- a/server/src/types/email.ts
+++ b/server/src/types/email.ts
@@ -5,6 +5,7 @@ export type EmailTransportConfig = Pick<
| "systemEmailHost"
| "systemEmailPort"
| "systemEmailAddress"
+ | "systemEmailDisplayName"
| "systemEmailPassword"
| "systemEmailUser"
| "systemEmailConnectionHost"
diff --git a/server/src/types/settings.ts b/server/src/types/settings.ts
index eb8452866..3d0d0d931 100644
--- a/server/src/types/settings.ts
+++ b/server/src/types/settings.ts
@@ -21,6 +21,7 @@ export interface Settings {
systemEmailHost?: string;
systemEmailPort?: number;
systemEmailAddress?: string;
+ systemEmailDisplayName?: string;
systemEmailPassword?: string;
systemEmailUser?: string;
systemEmailConnectionHost?: string;
diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts
index cf7478610..523cb2a6e 100644
--- a/server/src/validation/notificationValidation.ts
+++ b/server/src/validation/notificationValidation.ts
@@ -113,6 +113,7 @@ export const sendTestEmailBodyValidation = z.object({
systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(),
systemEmailAddress: z.string().optional(),
+ systemEmailDisplayName: z.string().optional(),
systemEmailPassword: z.string().optional(),
systemEmailUser: z.string().optional(),
systemEmailConnectionHost: z.union([z.string(), z.literal("")]).optional(),
diff --git a/server/src/validation/settingsValidation.ts b/server/src/validation/settingsValidation.ts
index f2c4e4c94..db58c50ed 100644
--- a/server/src/validation/settingsValidation.ts
+++ b/server/src/validation/settingsValidation.ts
@@ -14,6 +14,12 @@ export const updateAppSettingsBodyValidation = z
timezone: z.string().optional(),
systemEmailHost: z.string().nullable().optional(),
systemEmailAddress: z.string().nullable().optional(),
+ systemEmailDisplayName: z
+ .string()
+ .max(100)
+ .transform((val) => (val.trim() === "" ? null : val.trim()))
+ .nullable()
+ .optional(),
systemEmailPassword: z.string().nullable().optional(),
systemEmailUser: z.string().nullable().optional(),
systemEmailConnectionHost: z.string().nullable().optional(),
diff --git a/server/test/unit/services/emailService.test.ts b/server/test/unit/services/emailService.test.ts
index 13b234186..d4d275ad8 100644
--- a/server/test/unit/services/emailService.test.ts
+++ b/server/test/unit/services/emailService.test.ts
@@ -396,5 +396,80 @@ describe("EmailService", () => {
})
);
});
+
+ // ── from-header formatting ───────────────────────────────────────
+
+ describe("from header formatting", () => {
+ const sendAndGetFrom = async (overrides: Partial) => {
+ const transporter = createMockTransporter();
+ const nodemailer = createMockNodemailer(transporter);
+ const { service } = createService({ nodemailer });
+
+ await service.sendEmail("to@example.com", "Subject", "body
", makeTransportConfig(overrides));
+
+ const call = (transporter.sendMail as jest.Mock).mock.calls[0]?.[0] as { from: unknown } | undefined;
+ return call?.from;
+ };
+
+ it("uses bare address when systemEmailDisplayName is undefined", async () => {
+ const from = await sendAndGetFrom({
+ systemEmailAddress: "noreply@example.com",
+ systemEmailDisplayName: undefined,
+ });
+
+ expect(from).toBe("noreply@example.com");
+ });
+
+ it("uses bare address when systemEmailDisplayName is empty string", async () => {
+ const from = await sendAndGetFrom({
+ systemEmailAddress: "noreply@example.com",
+ systemEmailDisplayName: "",
+ });
+
+ expect(from).toBe("noreply@example.com");
+ });
+
+ it("uses bare address when systemEmailDisplayName is whitespace-only", async () => {
+ const from = await sendAndGetFrom({
+ systemEmailAddress: "noreply@example.com",
+ systemEmailDisplayName: " ",
+ });
+
+ expect(from).toBe("noreply@example.com");
+ });
+
+ it("uses {name, address} when display name and address are both set", async () => {
+ const from = await sendAndGetFrom({
+ systemEmailAddress: "noreply@example.com",
+ systemEmailDisplayName: "Checkmate notifications",
+ });
+
+ expect(from).toEqual({
+ name: "Checkmate notifications",
+ address: "noreply@example.com",
+ });
+ });
+
+ it("trims surrounding whitespace from display name", async () => {
+ const from = await sendAndGetFrom({
+ systemEmailAddress: "noreply@example.com",
+ systemEmailDisplayName: " Checkmate ",
+ });
+
+ expect(from).toEqual({
+ name: "Checkmate",
+ address: "noreply@example.com",
+ });
+ });
+
+ it("falls back to bare address when display name is set but address is empty", async () => {
+ const from = await sendAndGetFrom({
+ systemEmailAddress: "",
+ systemEmailDisplayName: "Checkmate",
+ });
+
+ expect(from).toBe("");
+ });
+ });
});
});