Skip to content

Commit

Permalink
feat: detect common free/disposable email providers locally (#1096)
Browse files Browse the repository at this point in the history
This PR regenerates the WASM to pull in some updates for our email validation primitive.
Notably it also provides functionality to detect the five most common free email providers locally, preventing a call to the Arcjet API.
This won't result in any behaviour change for email validation beyond potentially fewer api requests.

Closes #1095
  • Loading branch information
e-moran authored Jul 12, 2024
1 parent 10184f9 commit 115d016
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 78 deletions.
43 changes: 39 additions & 4 deletions analyze/edge-light.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,21 @@ import type {
EmailValidationConfig,
BotDetectionResult,
BotType,
EmailValidationResult,
} from "./wasm/arcjet_analyze_js_req.component.js";

import componentCoreWasm from "./wasm/arcjet_analyze_js_req.component.core.wasm?module";
import componentCore2Wasm from "./wasm/arcjet_analyze_js_req.component.core2.wasm?module";
import componentCore3Wasm from "./wasm/arcjet_analyze_js_req.component.core3.wasm?module";

const FREE_EMAIL_PROVIDERS = [
"gmail.com",
"yahoo.com",
"hotmail.com",
"aol.com",
"hotmail.co.uk",
];

interface AnalyzeContext {
log: ArcjetLogger;
characteristics: string[];
Expand Down Expand Up @@ -43,6 +52,23 @@ async function init(context: AnalyzeContext) {
log.error(msg);
},
},
"arcjet:js-req/email-validator-overrides": {
isFreeEmail(domain) {
if (FREE_EMAIL_PROVIDERS.includes(domain)) {
return "yes";
}
return "unknown";
},
isDisposableEmail() {
return "unknown";
},
hasMxRecords() {
return "unknown";
},
hasGravatar() {
return "unknown";
},
},
};

try {
Expand Down Expand Up @@ -97,14 +123,23 @@ export async function isValidEmail(
context: AnalyzeContext,
candidate: string,
options?: EmailValidationConfig,
) {
): Promise<EmailValidationResult> {
const analyze = await init(context);
const optionsOrDefault = {
requireTopLevelDomain: true,
allowDomainLiteral: false,
blockedEmails: [],
...options,
};

if (typeof analyze !== "undefined") {
return analyze.isValidEmail(candidate, options);
return analyze.isValidEmail(candidate, optionsOrDefault);
} else {
// TODO: Fallback to JS if we don't have WASM?
return true;
// Skip the local evaluation of the rule if WASM is not available
return {
validity: "valid",
blocked: [],
};
}
}

Expand Down
43 changes: 39 additions & 4 deletions analyze/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,21 @@ import type {
EmailValidationConfig,
BotDetectionResult,
BotType,
EmailValidationResult,
} from "./wasm/arcjet_analyze_js_req.component.js";

import { wasm as componentCoreWasm } from "./wasm/arcjet_analyze_js_req.component.core.wasm?js";
import { wasm as componentCore2Wasm } from "./wasm/arcjet_analyze_js_req.component.core2.wasm?js";
import { wasm as componentCore3Wasm } from "./wasm/arcjet_analyze_js_req.component.core3.wasm?js";

const FREE_EMAIL_PROVIDERS = [
"gmail.com",
"yahoo.com",
"hotmail.com",
"aol.com",
"hotmail.co.uk",
];

interface AnalyzeContext {
log: ArcjetLogger;
characteristics: string[];
Expand Down Expand Up @@ -57,6 +66,23 @@ async function init(context: AnalyzeContext) {
log.error(msg);
},
},
"arcjet:js-req/email-validator-overrides": {
isFreeEmail(domain) {
if (FREE_EMAIL_PROVIDERS.includes(domain)) {
return "yes";
}
return "unknown";
},
isDisposableEmail() {
return "unknown";
},
hasMxRecords() {
return "unknown";
},
hasGravatar() {
return "unknown";
},
},
};

try {
Expand Down Expand Up @@ -111,14 +137,23 @@ export async function isValidEmail(
context: AnalyzeContext,
candidate: string,
options?: EmailValidationConfig,
) {
): Promise<EmailValidationResult> {
const analyze = await init(context);
const optionsOrDefault = {
requireTopLevelDomain: true,
allowDomainLiteral: false,
blockedEmails: [],
...options,
};

if (typeof analyze !== "undefined") {
return analyze.isValidEmail(candidate, options);
return analyze.isValidEmail(candidate, optionsOrDefault);
} else {
// TODO: Fallback to JS if we don't have WASM?
return true;
// Skip the local evaluation of the rule if WASM is not available
return {
validity: "valid",
blocked: [],
};
}
}

Expand Down
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core.wasm
Binary file not shown.
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core2.wasm
Binary file not shown.
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core3.wasm
Binary file not shown.
21 changes: 18 additions & 3 deletions analyze/wasm/arcjet_analyze_js_req.component.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,33 @@ export interface BotDetectionResult {
botType: BotType,
botScore: number,
}
/**
* # Variants
*
* ## `"valid"`
*
* ## `"invalid"`
*/
export type EmailValidity = 'valid' | 'invalid';
export interface EmailValidationResult {
validity: EmailValidity,
blocked: string[],
}
export interface EmailValidationConfig {
requireTopLevelDomain?: boolean,
allowDomainLiteral?: boolean,
requireTopLevelDomain: boolean,
allowDomainLiteral: boolean,
blockedEmails: string[],
}
import { ArcjetJsReqEmailValidatorOverrides } from './interfaces/arcjet-js-req-email-validator-overrides.js';
import { ArcjetJsReqLogger } from './interfaces/arcjet-js-req-logger.js';
export interface ImportObject {
'arcjet:js-req/email-validator-overrides': typeof ArcjetJsReqEmailValidatorOverrides,
'arcjet:js-req/logger': typeof ArcjetJsReqLogger,
}
export interface Root {
detectBot(headers: string, patternsAdd: string, patternsRemove: string): BotDetectionResult,
generateFingerprint(request: string, characteristics: string[]): string,
isValidEmail(candidate: string, options: EmailValidationConfig | undefined): boolean,
isValidEmail(candidate: string, options: EmailValidationConfig): EmailValidationResult,
}

/**
Expand Down
Loading

0 comments on commit 115d016

Please sign in to comment.