fix: add utf8 email validation support (#6239)

Co-authored-by: Thomas <thomas@ajnart.fr>
This commit is contained in:
Nam NguyenandThomas authored and GitHub committed 2026-07-10 17:28:22 +02:00
1 parent 7c152cf4fe
commit 7d93df04dd
6 files changed
+43 -13

No files matched your search

@@ -17,6 +17,7 @@ import { showErrorNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { UserAvatar } from "@homarr/ui";
import { createCustomErrorParams } from "@homarr/validation/form/i18n";
import { optionalEmailSchema } from "@homarr/validation/email";
import { userPasswordSchema } from "@homarr/validation/user";
import { GroupSelectModal } from "~/components/access/group-select-modal";
@@ -60,7 +61,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
const generalForm = useZodForm(
z.object({
username: z.string().min(1),
email: z.string().email().or(z.string().length(0).optional()),
email: optionalEmailSchema,
}),
{
initialValues: {
@@ -1,5 +1,5 @@
import { CredentialsSignin } from "@auth/core/errors";
import { z } from "zod/v4";
import type { z } from "zod/v4";
import { createId } from "@homarr/common";
import { createLogger } from "@homarr/core/infrastructure/logs";
@@ -7,6 +7,7 @@ import type { Database, InferInsertModel } from "@homarr/db";
import { and, eq } from "@homarr/db";
import { users } from "@homarr/db/schema";
import type { ldapSignInSchema } from "@homarr/validation/user";
import { utf8EmailSchema } from "@homarr/validation/email";
import { env } from "../../../env";
import { LdapClient } from "../ldap-client";
@@ -52,8 +53,7 @@ export const authorizeWithLdapCredentialsAsync = async (
throw new CredentialsSignin(`User not found in LDAP username="${credentials.name}"`);
}
// Validate email
const mailResult = await z.string().email().safeParseAsync(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]);
const mailResult = await utf8EmailSchema.safeParseAsync(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]);
if (!mailResult.success) {
logger.error("User found in LDAP but with invalid or non-existing Email", {
+3 -1
View File
@@ -1,5 +1,7 @@
import { z } from "zod/v4";
import { requiredNullableEmailSchema } from "@homarr/validation/email";
const regexEncryptedSchema = z.string().regex(/^[a-f0-9]+\.[a-f0-9]+$/g);
const encryptedSchema = z.custom<`${string}.${string}`>((value) => regexEncryptedSchema.safeParse(value).success);
@@ -7,7 +9,7 @@ const encryptedSchema = z.custom<`${string}.${string}`>((value) => regexEncrypte
export const oldmarrImportUserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email().nullable(),
email: requiredNullableEmailSchema,
emailVerified: z.date().nullable(),
image: z.string().nullable(),
isAdmin: z.boolean(),
+16
View File
@@ -0,0 +1,16 @@
import { z } from "zod/v4";
export const utf8EmailSchema = z.email({ pattern: z.regexes.unicodeEmail });
export const optionalEmailSchema = utf8EmailSchema.or(z.string().length(0)).optional();
export const nullableEmailSchema = utf8EmailSchema
.or(z.literal(""))
.transform((value) => (value === "" ? null : value))
.optional()
.nullable();
export const requiredNullableEmailSchema = utf8EmailSchema
.or(z.literal(""))
.transform((value) => (value === "" ? null : value))
.nullable();
+16
View File
@@ -4,6 +4,7 @@ import { z } from "zod/v4";
import type { TranslationFunction } from "@homarr/translation";
import { createCustomErrorParams, zodErrorMap } from "./i18n";
import { utf8EmailSchema } from "../email";
const expectError = (error: z.core.$ZodIssue, key: string) => {
expect(error.message).toContain(key);
@@ -111,4 +112,19 @@ describe("i18n", () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expectError(result.error.issues[0]!, "boardAlreadyExists");
});
test("should validate UTF8SMTP email with umlauts", () => {
const result = utf8EmailSchema.safeParse("jörg@example.com");
expect(result.success).toBe(true);
});
test("should validate UTF8SMTP email with unicode domain", () => {
const result = utf8EmailSchema.safeParse("user@münchen.de");
expect(result.success).toBe(true);
});
test("should validate standard ASCII emails", () => {
const result = utf8EmailSchema.safeParse("user@example.com");
expect(result.success).toBe(true);
});
});
+3 -8
View File
@@ -6,6 +6,7 @@ import type { TranslationObject } from "@homarr/translation";
import { zodEnumFromArray } from "./enums";
import { createCustomErrorParams } from "./form/i18n";
import { nullableEmailSchema, optionalEmailSchema } from "./email";
// We always want the lowercase version of the username to compare it in a case-insensitive way
export const usernameSchema = z.string().trim().toLowerCase().min(3).max(255);
@@ -42,7 +43,7 @@ export const userBaseCreateSchema = z.object({
username: usernameSchema,
password: userPasswordSchema,
confirmPassword: z.string(),
email: z.string().email().or(z.string().length(0)).optional(),
email: optionalEmailSchema,
});
export const userCreateSchema = addConfirmPasswordRefinement(userBaseCreateSchema).and(
@@ -85,13 +86,7 @@ export const userRegistrationApiSchema = userRegistrationSchema.and(
export const userEditProfileSchema = z.object({
id: z.string(),
name: usernameSchema,
email: z
.string()
.email()
.or(z.literal(""))
.transform((value) => (value === "" ? null : value))
.optional()
.nullable(),
email: nullableEmailSchema,
});
const baseChangePasswordSchema = z.object({