From b34ca1543a133be2093b51e9ae7a9bc4bf64028b Mon Sep 17 00:00:00 2001 From: Ludovic Ortega Date: Mon, 20 Oct 2025 17:24:24 +0300 Subject: [PATCH] feat: do not enforce TLD on email (#2075) fix #1846 --- server/lib/notifications/agents/email.ts | 6 ++++-- server/routes/auth.ts | 2 +- src/components/Login/AddEmailModal.tsx | 7 ++++++- src/components/ResetPassword/RequestResetLink.tsx | 7 ++++++- .../Settings/Notifications/NotificationsEmail.tsx | 7 ++++++- src/components/Setup/JellyfinSetup.tsx | 7 ++++++- src/components/UserList/index.tsx | 7 ++++++- .../UserSettings/UserGeneralSettings/index.tsx | 15 ++++++++++++--- 8 files changed, 47 insertions(+), 11 deletions(-) diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 96963dfa..7b7a679f 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -221,7 +221,9 @@ class EmailAgent this.getSettings(), payload.notifyUser.settings?.pgpKey ); - if (validator.isEmail(payload.notifyUser.email)) { + if ( + validator.isEmail(payload.notifyUser.email, { require_tld: false }) + ) { await email.send( this.buildMessage( type, @@ -283,7 +285,7 @@ class EmailAgent this.getSettings(), user.settings?.pgpKey ); - if (validator.isEmail(user.email)) { + if (validator.isEmail(user.email, { require_tld: false })) { await email.send( this.buildMessage(type, payload, user.email, user.displayName) ); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index a96b63df..9f670f3c 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -37,7 +37,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => { const settings = await getSettings(); if ( settings.notifications.agents.email.options.userEmailRequired && - !validator.isEmail(user.email) + !validator.isEmail(user.email, { require_tld: false }) ) { user.warnings.push('userEmailRequired'); logger.warn(`User ${user.username} has no valid email address`); diff --git a/src/components/Login/AddEmailModal.tsx b/src/components/Login/AddEmailModal.tsx index cde0c8bd..0ae49131 100644 --- a/src/components/Login/AddEmailModal.tsx +++ b/src/components/Login/AddEmailModal.tsx @@ -5,6 +5,7 @@ import { Transition } from '@headlessui/react'; import axios from 'axios'; import { Field, Formik } from 'formik'; import { useIntl } from 'react-intl'; +import validator from 'validator'; import * as Yup from 'yup'; const messages = defineMessages('components.Login', { @@ -36,7 +37,11 @@ const AddEmailModal: React.FC = ({ const EmailSettingsSchema = Yup.object().shape({ email: Yup.string() - .email(intl.formatMessage(messages.validationEmailFormat)) + .test( + 'email', + intl.formatMessage(messages.validationEmailFormat), + (value) => !value || validator.isEmail(value, { require_tld: false }) + ) .required(intl.formatMessage(messages.validationEmailRequired)), }); diff --git a/src/components/ResetPassword/RequestResetLink.tsx b/src/components/ResetPassword/RequestResetLink.tsx index b331bb20..3bc06d49 100644 --- a/src/components/ResetPassword/RequestResetLink.tsx +++ b/src/components/ResetPassword/RequestResetLink.tsx @@ -10,6 +10,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { useState } from 'react'; import { useIntl } from 'react-intl'; +import validator from 'validator'; import * as Yup from 'yup'; const messages = defineMessages('components.ResetPassword', { @@ -29,7 +30,11 @@ const ResetPassword = () => { const ResetSchema = Yup.object().shape({ email: Yup.string() - .email(intl.formatMessage(messages.validationemailrequired)) + .test( + 'email', + intl.formatMessage(messages.validationemailrequired), + (value) => !value || validator.isEmail(value, { require_tld: false }) + ) .required(intl.formatMessage(messages.validationemailrequired)), }); diff --git a/src/components/Settings/Notifications/NotificationsEmail.tsx b/src/components/Settings/Notifications/NotificationsEmail.tsx index d88906a5..214d87da 100644 --- a/src/components/Settings/Notifications/NotificationsEmail.tsx +++ b/src/components/Settings/Notifications/NotificationsEmail.tsx @@ -11,6 +11,7 @@ import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR, { mutate } from 'swr'; +import validator from 'validator'; import * as Yup from 'yup'; const messages = defineMessages('components.Settings.Notifications', { @@ -77,7 +78,11 @@ const NotificationsEmail = () => { .required(intl.formatMessage(messages.validationEmail)), otherwise: Yup.string().nullable(), }) - .email(intl.formatMessage(messages.validationEmail)), + .test( + 'email', + intl.formatMessage(messages.validationEmail), + (value) => !value || validator.isEmail(value, { require_tld: false }) + ), smtpHost: Yup.string().when('enabled', { is: true, then: Yup.string() diff --git a/src/components/Setup/JellyfinSetup.tsx b/src/components/Setup/JellyfinSetup.tsx index 631f99fd..036ee8c3 100644 --- a/src/components/Setup/JellyfinSetup.tsx +++ b/src/components/Setup/JellyfinSetup.tsx @@ -8,6 +8,7 @@ import axios from 'axios'; import { Field, Form, Formik } from 'formik'; import { FormattedMessage, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; +import validator from 'validator'; import * as Yup from 'yup'; const messages = defineMessages('components.Login', { @@ -90,7 +91,11 @@ function JellyfinSetup({ (value) => !value || !value.endsWith('/') ), email: Yup.string() - .email(intl.formatMessage(messages.validationemailformat)) + .test( + 'email', + intl.formatMessage(messages.validationemailformat), + (value) => !value || validator.isEmail(value, { require_tld: false }) + ) .required(intl.formatMessage(messages.validationemailrequired)), username: Yup.string().required( intl.formatMessage(messages.validationusernamerequired) diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 877f95ae..d2cde505 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -36,6 +36,7 @@ import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; +import validator from 'validator'; import * as Yup from 'yup'; import JellyfinImportModal from './JellyfinImportModal'; @@ -210,7 +211,11 @@ const UserList = () => { ), email: Yup.string() .required() - .email(intl.formatMessage(messages.validationEmail)), + .test( + 'email', + intl.formatMessage(messages.validationEmail), + (value) => !value || validator.isEmail(value, { require_tld: false }) + ), password: Yup.lazy((value) => !value ? Yup.string() diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index 71476c5b..3445e7c1 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -23,6 +23,7 @@ import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; +import validator from 'validator'; import * as Yup from 'yup'; const messages = defineMessages( @@ -105,10 +106,18 @@ const UserGeneralSettings = () => { user?.id === 1 || (user?.userType !== UserType.JELLYFIN && user?.userType !== UserType.EMBY) ? Yup.string() - .email(intl.formatMessage(messages.validationemailformat)) + .test( + 'email', + intl.formatMessage(messages.validationemailformat), + (value) => + !value || validator.isEmail(value, { require_tld: false }) + ) .required(intl.formatMessage(messages.validationemailrequired)) - : Yup.string().email( - intl.formatMessage(messages.validationemailformat) + : Yup.string().test( + 'email', + intl.formatMessage(messages.validationemailformat), + (value) => + !value || validator.isEmail(value, { require_tld: false }) ), discordId: Yup.string() .nullable()