Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/api/views-dataroom/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions app/api/views/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
47 changes: 38 additions & 9 deletions components/view/access-form/email-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ".";

Expand Down Expand Up @@ -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
);
Expand All @@ -78,11 +92,26 @@ export default function EmailSection({
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
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 = () => {
Expand Down
96 changes: 96 additions & 0 deletions lib/utils/disposable-email-validator.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
7 changes: 7 additions & 0 deletions pages/api/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]";

Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) => ({
Expand Down
7 changes: 7 additions & 0 deletions pages/api/teams/[teamId]/invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]";

Expand All @@ -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: {
Expand Down