mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
feat: admin-configurable email blocklist (#2884)
This commit is contained in:
@@ -132,11 +132,16 @@ export const isEmailDomainAllowedForSignup = (email: string): boolean => {
|
||||
* Matching also covers subdomains (e.g. `foo.mailinator.com` resolves to
|
||||
* `mailinator.com`).
|
||||
*
|
||||
* An optional `additionalBlockedDomains` list can be supplied to layer
|
||||
* admin-configured custom domains on top of the bundled list. These are
|
||||
* matched with the same subdomain-walking behaviour and are expected to be
|
||||
* pre-normalised (trimmed + lowercased) by the caller.
|
||||
*
|
||||
* Returns `true` when the email is disposable and should be rejected.
|
||||
* Email format validation is intentionally NOT performed here — that is
|
||||
* handled by Zod upstream.
|
||||
*/
|
||||
export const isDisposableEmail = (email: string): boolean => {
|
||||
export const isDisposableEmail = (email: string, additionalBlockedDomains: string[] = []): boolean => {
|
||||
const domain = email.toLowerCase().split('@').pop();
|
||||
|
||||
if (!domain) {
|
||||
@@ -144,11 +149,12 @@ export const isDisposableEmail = (email: string): boolean => {
|
||||
}
|
||||
|
||||
const blacklist = MailChecker.blacklist();
|
||||
const blocklist = new Set(additionalBlockedDomains);
|
||||
|
||||
let currentDomain: string | undefined = domain;
|
||||
|
||||
while (currentDomain) {
|
||||
if (blacklist.has(currentDomain)) {
|
||||
if (blacklist.has(currentDomain) || blocklist.has(currentDomain)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { SITE_SETTINGS_EMAIL_BLOCKLIST_ID, ZSiteSettingsEmailBlocklistSchema } from './schemas/email-blocklist';
|
||||
|
||||
/**
|
||||
* Returns the list of admin-configured email domains that should be treated as
|
||||
* disposable / blocked, in addition to the bundled `mailchecker` list.
|
||||
*
|
||||
* Returns an empty array when the setting has not been configured, is
|
||||
* disabled, or fails to parse — so a misconfigured setting can never block
|
||||
* signups outright.
|
||||
*/
|
||||
export const getEmailBlocklistDomains = async (): Promise<string[]> => {
|
||||
const setting = await prisma.siteSettings.findFirst({
|
||||
where: {
|
||||
id: SITE_SETTINGS_EMAIL_BLOCKLIST_ID,
|
||||
},
|
||||
});
|
||||
|
||||
if (!setting || !setting.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsed = ZSiteSettingsEmailBlocklistSchema.safeParse(setting);
|
||||
|
||||
if (!parsed.success) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parsed.data.data.domains;
|
||||
};
|
||||
@@ -1,9 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZSiteSettingsBannerSchema } from './schemas/banner';
|
||||
import { ZSiteSettingsEmailBlocklistSchema } from './schemas/email-blocklist';
|
||||
import { ZSiteSettingsTelemetrySchema } from './schemas/telemetry';
|
||||
|
||||
export const ZSiteSettingSchema = z.union([ZSiteSettingsBannerSchema, ZSiteSettingsTelemetrySchema]);
|
||||
export const ZSiteSettingSchema = z.union([
|
||||
ZSiteSettingsBannerSchema,
|
||||
ZSiteSettingsEmailBlocklistSchema,
|
||||
ZSiteSettingsTelemetrySchema,
|
||||
]);
|
||||
|
||||
export type TSiteSettingSchema = z.infer<typeof ZSiteSettingSchema>;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZSiteSettingsBaseSchema } from './_base';
|
||||
|
||||
export const SITE_SETTINGS_EMAIL_BLOCKLIST_ID = 'email.blocklist-domains';
|
||||
|
||||
/**
|
||||
* Normalises a single domain entry: trims whitespace, lowercases, strips
|
||||
* a leading "@" if present (so users can paste either "bad.com" or "@bad.com").
|
||||
*/
|
||||
const normaliseDomain = (value: string): string => value.trim().toLowerCase().replace(/^@/, '');
|
||||
|
||||
const ZBlocklistDomainsSchema = z
|
||||
.array(z.string())
|
||||
.transform((values) => Array.from(new Set(values.map(normaliseDomain).filter((value) => value.length > 0))));
|
||||
|
||||
export const ZSiteSettingsEmailBlocklistSchema = ZSiteSettingsBaseSchema.extend({
|
||||
id: z.literal(SITE_SETTINGS_EMAIL_BLOCKLIST_ID),
|
||||
data: z
|
||||
.object({
|
||||
domains: ZBlocklistDomainsSchema.default([]),
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
domains: [],
|
||||
}),
|
||||
});
|
||||
|
||||
export type TSiteSettingsEmailBlocklistSchema = z.infer<typeof ZSiteSettingsEmailBlocklistSchema>;
|
||||
Reference in New Issue
Block a user