diff --git a/app/api/views-dataroom/route.ts b/app/api/views-dataroom/route.ts index 247b97916..ced8e3ce7 100644 --- a/app/api/views-dataroom/route.ts +++ b/app/api/views-dataroom/route.ts @@ -28,6 +28,7 @@ import { generateOTP } from "@/lib/utils/generate-otp"; import { LOCALHOST_IP } from "@/lib/utils/geo"; import { checkGlobalBlockList } from "@/lib/utils/global-block-list"; import { validateEmail } from "@/lib/utils/validate-email"; +import { checkDisposableEmail } from "@/lib/utils/disposable-email-validator"; export async function POST(request: NextRequest) { try { @@ -241,6 +242,15 @@ export async function POST(request: NextRequest) { { status: 400 }, ); } + + // Check if email uses a disposable domain + const disposableEmailCheck = checkDisposableEmail(email); + if (disposableEmailCheck.isDisposable) { + return NextResponse.json( + { message: disposableEmailCheck.error || "Disposable email addresses are not allowed." }, + { status: 400 }, + ); + } } // Check if password is required for visiting the link diff --git a/app/api/views/route.ts b/app/api/views/route.ts index 931f8bb20..b919477d0 100644 --- a/app/api/views/route.ts +++ b/app/api/views/route.ts @@ -25,6 +25,7 @@ import { generateOTP } from "@/lib/utils/generate-otp"; import { LOCALHOST_IP } from "@/lib/utils/geo"; import { checkGlobalBlockList } from "@/lib/utils/global-block-list"; import { validateEmail } from "@/lib/utils/validate-email"; +import { checkDisposableEmail } from "@/lib/utils/disposable-email-validator"; export async function POST(request: NextRequest) { try { @@ -177,6 +178,15 @@ export async function POST(request: NextRequest) { { status: 400 }, ); } + + // Check if email uses a disposable domain + const disposableEmailCheck = checkDisposableEmail(email); + if (disposableEmailCheck.isDisposable) { + return NextResponse.json( + { message: disposableEmailCheck.error || "Disposable email addresses are not allowed." }, + { status: 400 }, + ); + } } // Check if password is required for visiting the link diff --git a/components/view/access-form/email-section.tsx b/components/view/access-form/email-section.tsx index aeea8dbae..eee280921 100644 --- a/components/view/access-form/email-section.tsx +++ b/components/view/access-form/email-section.tsx @@ -6,6 +6,7 @@ import { useDebouncedCallback } from "use-debounce"; import { cn } from "@/lib/utils"; import { determineTextColor } from "@/lib/utils/determine-text-color"; import { validateEmail } from "@/lib/utils/validate-email"; +import { isDisposableEmail } from "@/lib/utils/disposable-email-validator"; import { DEFAULT_ACCESS_FORM_TYPE } from "."; @@ -46,14 +47,27 @@ export default function EmailSection({ const debouncedValidation = useDebouncedCallback( (value: string) => { - const isValid = !value || validateEmail(value); - if (isDirty && value && !isValid) { - setEmailError("Please enter a valid email address"); - } else { + if (!value) { setEmailError(null); + onValidationChange?.(true); + return; } + + const isValid = validateEmail(value); + const isDisposable = isValid ? isDisposableEmail(value) : false; + + if (isDirty && value) { + if (!isValid) { + setEmailError("Please enter a valid email address"); + } else if (isDisposable) { + setEmailError("Disposable email addresses are not allowed. Please use a permanent email address."); + } else { + setEmailError(null); + } + } + // Notify parent component about validation status - onValidationChange?.(isValid); + onValidationChange?.(isValid && !isDisposable); }, 500, // 500ms delay ); @@ -78,11 +92,26 @@ export default function EmailSection({ const handleBlur = (e: React.FocusEvent) => { setIsDirty(true); const value = e.target.value; - const isValid = !value || validateEmail(value); - if (value && !isValid) { - setEmailError("Please enter a valid email address"); + + if (!value) { + setEmailError(null); + onValidationChange?.(true); + return; + } + + const isValid = validateEmail(value); + const isDisposable = isValid ? isDisposableEmail(value) : false; + + if (value) { + if (!isValid) { + setEmailError("Please enter a valid email address"); + } else if (isDisposable) { + setEmailError("Disposable email addresses are not allowed. Please use a permanent email address."); + } else { + setEmailError(null); + } } - onValidationChange?.(isValid); + onValidationChange?.(isValid && !isDisposable); }; const handleFocus = () => { diff --git a/lib/utils/disposable-email-validator.ts b/lib/utils/disposable-email-validator.ts new file mode 100644 index 000000000..919b73ed7 --- /dev/null +++ b/lib/utils/disposable-email-validator.ts @@ -0,0 +1,96 @@ +import { extractEmailDomain } from "@/lib/utils/email-domain"; + +// Comprehensive list of disposable email domains +// Updated from https://github.com/disposable-email-domains/disposable-email-domains +// This list contains the most common disposable email services for performance +const DISPOSABLE_EMAIL_DOMAINS = new Set([ + // 10 minute mail services + "0-mail.com", "027168.com", "062e.com", "0815.ru", "0815.su", "0845.ru", "0box.eu", + "0cd.cn", "0clickemail.com", "0n0ff.net", "0nelce.com", "0rg.fr", "0v.ro", "0w.ro", + "0wnd.net", "0wnd.org", "0x207.info", "1-8.biz", "1-second-mail.site", "1-tm.com", + "10-minute-mail.com", "1000rebates.stream", "100likers.com", "105kg.ru", "10dk.email", + "10mail.com", "10mail.org", "10mail.tk", "10mail.xyz", "10minmail.de", "10minut.com.pl", + "10minut.xyz", "10minutemail.be", "10minutemail.cf", "10minutemail.co.uk", + "10minutemail.co.za", "10minutemail.com", "10minutemail.de", "10minutemail.ga", + "10minutemail.gq", "10minutemail.ml", "10minutemail.net", "10minutemail.nl", + "10minutemail.pro", "10minutemail.us", "10minutemailbox.com", "10minutemails.in", + "10minutenemail.de", "10minutenmail.xyz", "10x9.com", "11mail.com", "123-m.com", + "123mail.org", "12hourfreemail.com", "12minutemail.com", "12minutemail.net", + + // Common well-known disposable services + "guerrillamail.com", "guerrillamail.net", "guerrillamail.org", "guerrillamailblock.com", + "guerrillamail.biz", "guerrillamail.de", "grr.la", "sharklasers.com", + "temp-mail.org", "tempmail.org", "tempmail.com", "temp-mail.io", "temp-mail.net", + "throwaway.email", "throwawaymail.com", "yopmail.com", "yopmail.fr", "yopmail.net", + "mailinator.com", "mailinator.net", "mailinator.org", "mailinator2.com", + "getnada.com", "maildrop.cc", "dispostable.com", "tempail.com", "tempinbox.com", + "fakeinbox.com", "spamgourmet.com", "mailcatch.com", "trashmail.com", "trashmail.net", + "fakemailgenerator.com", "minute.email.com", "e4ward.com", "no-spam.ws", "spam4.me", + "trbvm.com", "emailondeck.com", "mytrashmail.com", "tempemailer.com", "tempemail.com", + + // 20 minute mail services + "20minutemail.com", "30minutemail.com", "60minutemail.com", "33mail.com", "7days-email.com", + + // Other common patterns + "1337.cf", "13tm.com", "1471.ru", "14n.co.uk", "150mail.com", "15qm.com", "1661.net", + "2120001.net", "321mail.com", "365-mail.tk", "365box.org", "365email.org", "365temporary.com", + "3d-game.com", "3mail.ga", "4-5.live", "4chanmail.com", "4gw.pw", "4mail.cf", "4mail.ga", + "4warding.com", "4warding.net", "4warding.org", "50e.info", "5amigos.com", "5emails.com", + "5mail.cf", "5mail.ga", "675hosting.com", "675hosting.net", "675hosting.org", "69.fa.gq", + "6ip.us", "6mail.cf", "6mail.ga", "6mail.ml", "6paq.com", "6url.com", "75hosting.com", + "75hosting.net", "75hosting.org", "7mail.ga", "7mail.ml", "8125.me", "8mail.cf", + "8mail.ga", "8mail.ml", "8startpage.com", "9mail.cf", "9ox.net", + + // Additional popular temporary email services + "MailDrop.cc", "nada.email", "mohmal.com", "emailfake.com", "throwam.com", + "incognitomail.org", "anonymbox.com", "sogetthis.com", "spamherald.com", + "spamstack.net", "spamthis.co.uk", "tempemail.co.uk", "tempemail.net", "tempsky.com", + "thankyou2010.com", "trash2009.com", "trashdevil.com", "trashemail.de", "trashymail.com", + "tyldd.com", "uggsrock.com", "wegwerfmail.de", "wegwerfmail.net", "wegwerfmail.org", + "wetrainbayarea.com", "wetrainbayarea.org", "wh4f.org", "whyspam.me", "willselfdestruct.com", + "xoxy.net", "yogamaven.com", "zoemail.org", "zoemail.net", "zzz.com", +]); + +/** + * Checks if an email address uses a disposable email domain + * @param email The email address to check + * @returns boolean indicating if the email uses a disposable domain + */ +export function isDisposableEmail(email: string): boolean { + if (!email || typeof email !== "string") { + return false; + } + + const domain = extractEmailDomain(email); + if (!domain) { + return false; + } + + // Remove the @ prefix from the domain if it exists + const cleanDomain = domain.startsWith("@") ? domain.slice(1) : domain; + + return DISPOSABLE_EMAIL_DOMAINS.has(cleanDomain.toLowerCase()); +} + +/** + * Validates email against disposable domains and returns error info + * @param email The email address to validate + * @returns object with isDisposable flag and optional error message + */ +export function checkDisposableEmail(email: string): { + isDisposable: boolean; + error?: string; +} { + if (!email || typeof email !== "string") { + return { isDisposable: false }; + } + + const isDisposable = isDisposableEmail(email); + + return { + isDisposable, + error: isDisposable + ? "Disposable email addresses are not allowed. Please use a permanent email address." + : undefined, + }; +} \ No newline at end of file diff --git a/pages/api/account/index.ts b/pages/api/account/index.ts index c66a50d3f..0b7851751 100644 --- a/pages/api/account/index.ts +++ b/pages/api/account/index.ts @@ -13,6 +13,7 @@ import prisma from "@/lib/prisma"; import { ratelimit, redis } from "@/lib/redis"; import { CustomUser } from "@/lib/types"; import { trim } from "@/lib/utils"; +import { checkDisposableEmail } from "@/lib/utils/disposable-email-validator"; import { authOptions } from "../auth/[...nextauth]"; @@ -40,6 +41,12 @@ export default async function handle( try { if (email && email !== sessionUser.email) { + // Check if email uses a disposable domain + const disposableEmailCheck = checkDisposableEmail(email); + if (disposableEmailCheck.isDisposable) { + throw new Error(disposableEmailCheck.error || "Disposable email addresses are not allowed."); + } + const userWithEmail = await prisma.user.findUnique({ where: { email, diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index eb7e9531a..d93888e22 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -17,6 +17,7 @@ import prisma from "@/lib/prisma"; import { CreateUserEmailProps, CustomUser } from "@/lib/types"; import { subscribe } from "@/lib/unsend"; import { generateChecksum } from "@/lib/utils/generate-checksum"; +import { isDisposableEmail } from "@/lib/utils/disposable-email-validator"; const VERCEL_DEPLOYMENT = !!process.env.VERCEL_URL; @@ -126,7 +127,7 @@ export const authOptions: NextAuthOptions = { }, callbacks: { signIn: async ({ user }) => { - if (!user.email || (await isBlacklistedEmail(user.email))) { + if (!user.email || (await isBlacklistedEmail(user.email)) || isDisposableEmail(user.email)) { await identifyUser(user.email ?? user.id); await trackAnalytics({ event: "User Sign In Attempted", diff --git a/pages/api/teams/[teamId]/datarooms/[id]/groups/[groupId]/members/index.ts b/pages/api/teams/[teamId]/datarooms/[id]/groups/[groupId]/members/index.ts index 3f4869893..54207a7c2 100644 --- a/pages/api/teams/[teamId]/datarooms/[id]/groups/[groupId]/members/index.ts +++ b/pages/api/teams/[teamId]/datarooms/[id]/groups/[groupId]/members/index.ts @@ -5,6 +5,7 @@ import { getServerSession } from "next-auth/next"; import prisma from "@/lib/prisma"; import { CustomUser } from "@/lib/types"; +import { checkDisposableEmail } from "@/lib/utils/disposable-email-validator"; export default async function handle( req: NextApiRequest, @@ -64,6 +65,16 @@ export default async function handle( return res.status(404).end("Group not found"); } + // Check for disposable email addresses + for (const email of emails) { + const disposableEmailCheck = checkDisposableEmail(email); + if (disposableEmailCheck.isDisposable) { + return res.status(400).json({ + error: `${email}: ${disposableEmailCheck.error || "Disposable email addresses are not allowed."}`, + }); + } + } + // First, create or connect viewers await prisma.viewer.createMany({ data: emails.map((email) => ({ diff --git a/pages/api/teams/[teamId]/invite.ts b/pages/api/teams/[teamId]/invite.ts index 69e0d97cb..1c7356525 100644 --- a/pages/api/teams/[teamId]/invite.ts +++ b/pages/api/teams/[teamId]/invite.ts @@ -11,6 +11,7 @@ import prisma from "@/lib/prisma"; import { CustomUser } from "@/lib/types"; import { generateChecksum } from "@/lib/utils/generate-checksum"; import { generateJWT } from "@/lib/utils/generate-jwt"; +import { checkDisposableEmail } from "@/lib/utils/disposable-email-validator"; import { authOptions } from "../../auth/[...nextauth]"; @@ -33,6 +34,12 @@ export default async function handle( return res.status(400).json("Email is missing in request body"); } + // Check if email uses a disposable domain + const disposableEmailCheck = checkDisposableEmail(email); + if (disposableEmailCheck.isDisposable) { + return res.status(400).json(disposableEmailCheck.error || "Disposable email addresses are not allowed."); + } + try { const team = await prisma.team.findUnique({ where: {