Email validation reference
Arcjet lets you validate and verify an email address. This is useful for preventing users from signing up with fake email addresses and can significantly reduce the amount of spam or fraudulent accounts.
How it works
Section titled “How it works”Validation
Section titled “Validation”The first step is to validate the email address syntax. This runs locally within the SDK and validates the email address is in the correct format. Validation is performed against several RFCs with modifications to exclude email addresses you wouldn’t expect signing up to a real application, such as local domains.
Two of the validation options are configurable. The first is whether two parts
of the domain are required, which defaults to required. The second is whether
domain literals such as example@[127.0.0.1] are allowed, which defaults to not
allowed. The SDK documentation describes how to configure them.
Internally, validation is implemented using the open source
email_address Crate.
Validation errors
Section titled “Validation errors”If an email address fails validation, Arcjet provides one of the following errors:
Configurable
Section titled “Configurable”MissingTopLevelDomain: The email address didn’t contain at least 2 domain segments – it must contain at least the domain name and TLD.InvalidDomainLiteral: The address contained a domain literal, such asfoo@[123.456.789.0], which Arcjet does not support.
Non-configurable
Section titled “Non-configurable”MissingSeparator: The@character was missing between the local part and domain.InvalidCharacter: The email address contained an invalid character.LocalPartEmpty: The local part of the email address must contain at least 1 character.LocalPartTooLong: The local part has a maximum length of 64 characters, as per RFC 3696.DomainEmpty: The domain of the email address must contain at least 1 character.DomainTooLong: The domain has a maximum length of 254 characters, as per RFC 3696 errata 1690.SubDomainEmpty: One or more domain segments, separated by the.character, are empty.SubDomainTooLong: One or more domain segments, separated by the.character, are longer than 63 characters, as per RFC 1034.
Verification
Section titled “Verification”If the email syntax is valid, the SDK passes the email address to the Arcjet cloud API for verification. This performs several checks:
- MX validation: checks whether the domain has valid MX records.
- Email type: checks whether the email address is a free, disposable, or role-based email address.
- Has Gravatar: checks whether the email address has a Gravatar image associated with it. This is useful for checking if the email address is a real person.
This metadata is returned as part of the Arcjet SDK response so you can decide what to do next. For example, you might decide to block signups from domains without MX records and from disposable email addresses, but add a flag to manually review those from free email providers.
Email types
Section titled “Email types”The Arcjet SDK provides an analysis of the email type, returning one or several of the following:
- Disposable: The email address is disposable, which means it’s registered to a service that allows throwaway email addresses. Although these are sometimes used for privacy, they are also often used for spam signups or fraudulent activity when combined with a transaction, such as attempting to use a credit card. We recommend blocking these in higher risk scenarios.
- Free: The email address is registered to a free email service. These are very common, such as GMail or Yahoo Mail, so we do not recommend blocking these. However, you may wish to flag these for review the first time they attempt a transaction.
- No MX records: This email address is registered to a domain name which has no MX records configured. This means it cannot receive email. We recommend blocking these.
- No Gravatar: This email has no Gravatar attached to the email from which makes it slightly less likely to be a valid signup. This can be used for your own risk scoring or to trigger a manual review.
Configuration
Section titled “Configuration”Email validation is configured by specifying the email types you wish to allow or deny, and whether you wish to modify certain validation options.
The arcjet client can be configured with one or more Email validation rules,
which are constructed with the validateEmail(options: EmailOptions) function
and configured by EmailOptions:
type EmailOptions = | { mode?: ArcjetMode | undefined; allow: ArcjetEmailType[]; deny?: never | undefined; requireTopLevelDomain?: boolean | undefined; allowDomainLiteral?: boolean | undefined; } | { mode?: ArcjetMode | undefined; allow?: never | undefined; deny: ArcjetEmailType[]; requireTopLevelDomain?: boolean | undefined; allowDomainLiteral?: boolean | undefined; };type ArcjetMode = "LIVE" | "DRY_RUN";type ArcjetEmailType = "DISPOSABLE" | "FREE" | "NO_MX_RECORDS" | "NO_GRAVATAR" | "INVALID";The allow and deny lists are mutually exclusive. With allow, Arcjet denies
any email type that the list does not name. With deny, Arcjet allows any email
type that the list does not name.
The validation options can usually be left as the defaults. However, if you wish to allow certain types of email addresses, you can modify the options:
requireTopLevelDomain: Whether or not to allow email addresses that don’t contain at least 2 domain segments (the domain name and TLD). Defaults totrue. Changing tofalsemeans thatfoo@barwould be allowed.allowDomainLiteral: Whether or not to allow email addresses with domain literals. Defaults tofalse. Changing totruemeans thatfoo@[123.456.789.0]would be allowed.
Pages and server actions
Arcjet can be used inside Next.js middleware, API routes, pages, server components, and server actions. Client components cannot be protected because they run on the client only.
See the Next.js SDK reference for examples of pages and page components and server actions.
Decision
Section titled “Decision”Arcjet provides a single protect function that is used to execute your
protection rules. This requires a request argument which is the request
context as passed to the request handler. When configured with a validateEmail
rule it also requires an additional email prop.
Arcjet can be integrated using a global guard or per route guards. However these can’t be used for signup protection rules because you can’t access the decision to handle the form submission. Instead, you call the protect function in your route handler. For an example where guards are used, see bot protection.
This function returns a Promise that resolves to an
ArcjetDecision object. This contains the following properties:
id(string) – The unique ID for the request. This can be used to look up the request in the Arcjet dashboard. It is prefixed withreq_for decisions involving the Arcjet cloud API. For decisions taken locally, the prefix islreq_.conclusion(ArcjetConclusion) – The final conclusion based on evaluating each of the configured rules. If you wish to accept Arcjet’s recommended action based on the configured rules then you can use this property.reason(ArcjetReason) – An object containing more detailed information about the conclusion.results(ArcjetRuleResult[]) – An array ofArcjetRuleResultobjects containing the results of each rule that was executed.ip(ArcjetIpDetails) – An object containing Arcjet’s analysis of the client IP address. For more information, see the SDK reference.
To check whether an email validation rule returned a deny conclusion, use
decision.isDenied() and decision.reason.isEmail() (JS) /
decision.is_denied() and decision.reason_v2.type == "EMAIL" (Python).
You can iterate through the results and check whether an email validation rule was applied:
for (const result of decision.results) { console.log("Rule Result", result);}for result in decision.results: print("Rule Result", result)This example logs the full result as well as the email validation rule:
import loggingimport os
from arcjet import EmailType, Mode, arcjet, detect_bot, validate_emailfrom fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponse
app = FastAPI()
logger = logging.getLogger(__name__)
aj = arcjet( key=os.environ["ARCJET_KEY"], # Get your site key from https://app.arcjet.com rules=[ validate_email( mode=Mode.LIVE, deny=[EmailType.DISPOSABLE], ), detect_bot( mode=Mode.LIVE, allow=[], # "allow none" will block all detected bots ), ],)
@app.post("/")async def index(request: Request): # email = request.form.get("email", "") email = "test@0zc7eznv3rsiswlohu.tk" # Disposable email for demo logger.info("Email received: %s", email)
decision = await aj.protect(request, email=email) logger.info("Arcjet decision %s", decision)
for result in decision.results: logger.info("Rule Result %s", result)
if result.reason_v2.type == "EMAIL": logger.info("Email rule %s", result)
if result.reason_v2.type == "BOT": logger.info("Bot protection rule %s", result)
if decision.is_denied(): if decision.reason_v2.type == "EMAIL": return JSONResponse({"error": "Invalid email"}, status_code=400) return JSONResponse({"error": "Forbidden"}, status_code=403)
return {"message": "Hello world", "email": email}import loggingimport os
from arcjet import EmailType, Mode, arcjet_sync, detect_bot, validate_emailfrom flask import Flask, jsonify, request
app = Flask(__name__)
logger = logging.getLogger(__name__)
aj = arcjet_sync( key=os.environ["ARCJET_KEY"], # Get your site key from https://app.arcjet.com rules=[ validate_email( mode=Mode.LIVE, deny=[EmailType.DISPOSABLE], ), detect_bot( mode=Mode.LIVE, allow=[], # "allow none" will block all detected bots ), ],)
@app.post("/")def index(): # email = request.form.get("email", "") email = "test@0zc7eznv3rsiswlohu.tk" # Disposable email for demo logger.info("Email received: %s", email)
decision = aj.protect(request, email=email) logger.info("Arcjet decision %s", decision)
for result in decision.results: logger.info("Rule Result %s", result)
if result.reason_v2.type == "EMAIL": logger.info("Email rule %s", result)
if result.reason_v2.type == "BOT": logger.info("Bot protection rule %s", result)
if decision.is_denied(): if decision.reason_v2.type == "EMAIL": return jsonify(error="Invalid email"), 400 return jsonify(error="Forbidden"), 403
return jsonify(message="Hello world", email=email)import arcjet, { detectBot, validateEmail } from "@arcjet/bun";import { env } from "bun";
const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default { port: 3000, fetch: aj.handler(async (req) => { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return new Response("Forbidden", { status: 403 }); }
return new Response("Hello world"); }),};import arcjet, { detectBot, validateEmail } from "@arcjet/bun";import { env } from "bun";
const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default { port: 3000, fetch: aj.handler(async (req) => { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return new Response("Forbidden", { status: 403 }); }
return new Response("Hello world"); }),};import arcjet, { validateEmail, detectBot } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(req: Request) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}import arcjet, { validateEmail, detectBot } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(req) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}import arcjet, { detectBot, validateEmail } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default async function handler(req, res) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}import arcjet, { detectBot, validateEmail } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}import { env } from "$env/dynamic/private";import arcjet, { detectBot, validateEmail } from "@arcjet/sveltekit";import { error, json, type RequestEvent } from "@sveltejs/kit";
const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(event: RequestEvent) { const decision = await aj.protect(event, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return error(403, "Forbidden"); }
return json({ message: "Hello world" });}import { env } from "$env/dynamic/private";import arcjet, { detectBot, validateEmail } from "@arcjet/sveltekit";import { error, json } from "@sveltejs/kit";
const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(event) { const decision = await aj.protect(event, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return error(403, "Forbidden"); }
return json({ message: "Hello world" });}import arcjet, { shield, validateEmail } from "@arcjet/remix";import type { ActionFunctionArgs } from "@remix-run/node";
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), shield({ mode: "LIVE", }), ],});
export async function action(args: ActionFunctionArgs) { // The request body is a FormData object //const formData = await args.request.formData(); //const email = formData.get("email") as string; const email = "test@0zc7eznv3rsiswlohu.tk"; // Disposable email for demo
const decision = await aj.protect(args, { email });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); }
if (result.reason.isShield()) { console.log("Shield protection rule", result); } }
if (decision.isDenied()) { return Response.json({ error: "Forbidden." }, { status: 403 }); }
// We don't need to use the decision elsewhere, but you could return it to // the component return null;}import arcjet, { shield, validateEmail } from "@arcjet/remix";
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), shield({ mode: "LIVE", }), ],});
export async function action(args) { // The request body is a FormData object //const formData = await args.request.formData(); //const email = formData.get("email") as string; const email = "test@0zc7eznv3rsiswlohu.tk"; // Disposable email for demo
const decision = await aj.protect(args, { email });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); }
if (result.reason.isShield()) { console.log("Shield protection rule", result); } }
if (decision.isDenied()) { return Response.json({ error: "Forbidden." }, { status: 403 }); }
// We don't need to use the decision elsewhere, but you could return it to // the component return null;}import arcjet, { detectBot, validateEmail } from "@arcjet/node";import express from "express";
const app = express();const port = 3000;
app.use(express.urlencoded({ extended: false }));
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
app.post("/", async (req, res) => { //const email = req.body.email; const email = "test@0zc7eznv3rsiswlohu.tk"; // Disposable email for demo console.log("Email received: ", email);
const decision = await aj.protect(req, { email }); console.log("Arcjet decision", decision);
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); }
if (result.reason.isBot()) { console.log("Bot protection rule", result); } }
if (decision.isDenied()) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Forbidden" })); } else { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello World", email })); }});
app.listen(port, () => { console.log(`Example app listening on port ${port}`);});import arcjet, { detectBot, validateEmail } from "@arcjet/node";import express from "express";
const app = express();const port = 3000;
app.use(express.urlencoded({ extended: false }));
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
app.post("/", async (req, res) => { //const email = req.body.email; const email = "test@0zc7eznv3rsiswlohu.tk"; // Disposable email for demo console.log("Email received: ", email);
const decision = await aj.protect(req, { email }); console.log("Arcjet decision", decision);
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); }
if (result.reason.isBot()) { console.log("Bot protection rule", result); } }
if (decision.isDenied()) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Forbidden" })); } else { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello World", email })); }});
app.listen(port, () => { console.log(`Example app listening on port ${port}`);});import { ARCJET, type ArcjetNest, detectBot, validateEmail,} from "@arcjet/nest";import { Body, Controller, HttpException, HttpStatus, Inject, Injectable, Logger, Post, Req, UseInterceptors,} from "@nestjs/common";import { NoFilesInterceptor } from "@nestjs/platform-express";import { IsNotEmpty } from "class-validator";import type { Request } from "express";
// Validation class as described at// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail// decorator here because Arcjet handles this for you.export class SignupDto { @IsNotEmpty() // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned email: string;}
// This would normally go in your service file e.g.// src/signup/signup.service.ts@Injectable()export class SignupService { private readonly logger = new Logger(SignupService.name);
signup(email: string): { message: string } { this.logger.log(`Form submission: ${email}`);
return { message: "Hello world", }; }}
// This would normally go in your controller file e.g.// src/signup/signup.controller.ts@Controller("signup")export class SignupController { private readonly logger = new Logger(SignupController.name);
constructor( private readonly signupService: SignupService, @Inject(ARCJET) private readonly arcjet: ArcjetNest, ) { }
// Implement a form handler following // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't // compatible with the NestJS Fastify adapter. @Post() @UseInterceptors(NoFilesInterceptor()) async index(@Req() req: Request, @Body() body: SignupDto) { const decision = await this.arcjet .withRule( detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only // configured with a list of bots to allow from // https://arcjet.com/bot-list allow: [], // blocks all automated clients }), ) .withRule( validateEmail({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only // block disposable, invalid, and email addresses with no MX records deny: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], }), ) .protect(req, { email: body.email });
this.logger.log(`Arcjet: id = ${decision.id}`); this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
for (const result of decision.results) { this.logger.log("Rule Result", result);
if (result.reason.isBot()) { this.logger.log("Bot protection rule", result); }
if (result.reason.isEmail()) { this.logger.log("Email validation rule", result); } }
if (decision.isDenied()) { if (decision.reason.isEmail()) { this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`);
let message: string;
// These are specific errors to help the user, but will also reveal the // validation to a spammer. if (decision.reason.emailTypes.includes("INVALID")) { message = "email address format is invalid. Is there a typo?"; } else if (decision.reason.emailTypes.includes("DISPOSABLE")) { message = "we do not allow disposable email addresses."; } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { message = "your email domain does not have an MX record. Is there a typo?"; } else { // This is a catch all, but the above should be exhaustive based on the // configured rules. message = "invalid email."; }
throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST); } else { throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); } }
return this.signupService.signup(body.email); }}Allow specific email types
Section titled “Allow specific email types”In addition to being able to deny specific email types, you can also configure Arcjet to only allow specific email types and Arcjet blocks all other types.
import loggingimport os
from arcjet import EmailType, Mode, arcjet, detect_bot, validate_emailfrom fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponse
app = FastAPI()
logger = logging.getLogger(__name__)
aj = arcjet( key=os.environ["ARCJET_KEY"], # Get your site key from https://app.arcjet.com rules=[ validate_email( mode=Mode.LIVE, allow=[EmailType.DISPOSABLE], ), detect_bot( mode=Mode.LIVE, allow=[], # "allow none" will block all detected bots ), ],)
@app.post("/")async def index(request: Request): # email = request.form.get("email", "") email = "test@0zc7eznv3rsiswlohu.tk" # Disposable email for demo logger.info("Email received: %s", email)
decision = await aj.protect(request, email=email) logger.info("Arcjet decision %s", decision)
for result in decision.results: logger.info("Rule Result %s", result)
if result.reason_v2.type == "EMAIL": logger.info("Email rule %s", result)
if result.reason_v2.type == "BOT": logger.info("Bot protection rule %s", result)
if decision.is_denied(): if decision.reason_v2.type == "EMAIL": return JSONResponse({"error": "Invalid email"}, status_code=400) return JSONResponse({"error": "Forbidden"}, status_code=403)
return {"message": "Hello world", "email": email}import loggingimport os
from arcjet import EmailType, Mode, arcjet_sync, detect_bot, validate_emailfrom flask import Flask, jsonify, request
app = Flask(__name__)
logger = logging.getLogger(__name__)
aj = arcjet_sync( key=os.environ["ARCJET_KEY"], # Get your site key from https://app.arcjet.com rules=[ validate_email( mode=Mode.LIVE, allow=[EmailType.DISPOSABLE], ), detect_bot( mode=Mode.LIVE, allow=[], # "allow none" will block all detected bots ), ],)
@app.post("/")def index(): # email = request.form.get("email", "") email = "test@0zc7eznv3rsiswlohu.tk" # Disposable email for demo logger.info("Email received: %s", email)
decision = aj.protect(request, email=email) logger.info("Arcjet decision %s", decision)
for result in decision.results: logger.info("Rule Result %s", result)
if result.reason_v2.type == "EMAIL": logger.info("Email rule %s", result)
if result.reason_v2.type == "BOT": logger.info("Bot protection rule %s", result)
if decision.is_denied(): if decision.reason_v2.type == "EMAIL": return jsonify(error="Invalid email"), 400 return jsonify(error="Forbidden"), 403
return jsonify(message="Hello world", email=email)import arcjet, { detectBot, validateEmail } from "@arcjet/bun";import { env } from "bun";
const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default { port: 3000, fetch: aj.handler(async (req) => { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return new Response("Forbidden", { status: 403 }); }
return new Response("Hello world"); }),};import arcjet, { detectBot, validateEmail } from "@arcjet/bun";import { env } from "bun";
const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default { port: 3000, fetch: aj.handler(async (req) => { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return new Response("Forbidden", { status: 403 }); }
return new Response("Hello world"); }),};import arcjet, { validateEmail, detectBot } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(req: Request) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}import arcjet, { validateEmail, detectBot } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(req) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Hello world", });}import arcjet, { detectBot, validateEmail } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default async function handler(req, res) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}import arcjet, { detectBot, validateEmail } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}import { env } from "$env/dynamic/private";import arcjet, { detectBot, validateEmail } from "@arcjet/sveltekit";import { error, json, type RequestEvent } from "@sveltejs/kit";
const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(event: RequestEvent) { const decision = await aj.protect(event, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return error(403, "Forbidden"); }
return json({ message: "Hello world" });}import { env } from "$env/dynamic/private";import arcjet, { detectBot, validateEmail } from "@arcjet/sveltekit";import { error, json } from "@sveltejs/kit";
const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
export async function POST(event) { const decision = await aj.protect(event, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); } }
if (decision.isDenied()) { return error(403, "Forbidden"); }
return json({ message: "Hello world" });}import arcjet, { shield, validateEmail } from "@arcjet/remix";import type { ActionFunctionArgs } from "@remix-run/node";
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), shield({ mode: "LIVE", }), ],});
export async function action(args: ActionFunctionArgs) { // The request body is a FormData object //const formData = await args.request.formData(); //const email = formData.get("email") as string; const email = "test@0zc7eznv3rsiswlohu.tk"; // Disposable email for demo
const decision = await aj.protect(args, { email });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); }
if (result.reason.isShield()) { console.log("Shield protection rule", result); } }
if (decision.isDenied()) { return Response.json({ error: "Forbidden." }, { status: 403 }); }
// We don't need to use the decision elsewhere, but you could return it to // the component return null;}import arcjet, { shield, validateEmail } from "@arcjet/remix";
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), shield({ mode: "LIVE", }), ],});
export async function action(args) { // The request body is a FormData object //const formData = await args.request.formData(); //const email = formData.get("email") as string; const email = "test@0zc7eznv3rsiswlohu.tk"; // Disposable email for demo
const decision = await aj.protect(args, { email });
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); }
if (result.reason.isShield()) { console.log("Shield protection rule", result); } }
if (decision.isDenied()) { return Response.json({ error: "Forbidden." }, { status: 403 }); }
// We don't need to use the decision elsewhere, but you could return it to // the component return null;}import arcjet, { detectBot, validateEmail } from "@arcjet/node";import express from "express";
const app = express();const port = 3000;
app.use(express.urlencoded({ extended: false }));
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
app.post("/", async (req, res) => { //const email = req.body.email; const email = "test@0zc7eznv3rsiswlohu.tk"; // Disposable email for demo console.log("Email received: ", email);
const decision = await aj.protect(req, { email }); console.log("Arcjet decision", decision);
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); }
if (result.reason.isBot()) { console.log("Bot protection rule", result); } }
if (decision.isDenied()) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Forbidden" })); } else { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello World", email })); }});
app.listen(port, () => { console.log(`Example app listening on port ${port}`);});import arcjet, { detectBot, validateEmail } from "@arcjet/node";import express from "express";
const app = express();const port = 3000;
app.use(express.urlencoded({ extended: false }));
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", allow: ["DISPOSABLE"], }), detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ],});
app.post("/", async (req, res) => { //const email = req.body.email; const email = "test@0zc7eznv3rsiswlohu.tk"; // Disposable email for demo console.log("Email received: ", email);
const decision = await aj.protect(req, { email }); console.log("Arcjet decision", decision);
for (const result of decision.results) { console.log("Rule Result", result);
if (result.reason.isEmail()) { console.log("Email rule", result); }
if (result.reason.isBot()) { console.log("Bot protection rule", result); } }
if (decision.isDenied()) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Forbidden" })); } else { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello World", email })); }});
app.listen(port, () => { console.log(`Example app listening on port ${port}`);});import { ARCJET, type ArcjetNest, detectBot, validateEmail,} from "@arcjet/nest";import { Body, Controller, HttpException, HttpStatus, Inject, Injectable, Logger, Post, Req, UseInterceptors,} from "@nestjs/common";import { NoFilesInterceptor } from "@nestjs/platform-express";import { IsNotEmpty } from "class-validator";import type { Request } from "express";
// Validation class as described at// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail// decorator here because Arcjet handles this for you.export class SignupDto { @IsNotEmpty() // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned email: string;}
// This would normally go in your service file e.g.// src/signup/signup.service.ts@Injectable()export class SignupService { private readonly logger = new Logger(SignupService.name);
signup(email: string): { message: string } { this.logger.log(`Form submission: ${email}`);
return { message: "Hello world", }; }}
// This would normally go in your controller file e.g.// src/signup/signup.controller.ts@Controller("signup")export class SignupController { private readonly logger = new Logger(SignupController.name);
constructor( private readonly signupService: SignupService, @Inject(ARCJET) private readonly arcjet: ArcjetNest, ) { }
// Implement a form handler following // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't // compatible with the NestJS Fastify adapter. @Post() @UseInterceptors(NoFilesInterceptor()) async index(@Req() req: Request, @Body() body: SignupDto) { const decision = await this.arcjet .withRule( detectBot({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only // configured with a list of bots to allow from // https://arcjet.com/bot-list allow: [], // blocks all automated clients }), ) .withRule( validateEmail({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only // block disposable, invalid, and email addresses with no MX records allow: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], }), ) .protect(req, { email: body.email });
this.logger.log(`Arcjet: id = ${decision.id}`); this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
for (const result of decision.results) { this.logger.log("Rule Result", result);
if (result.reason.isBot()) { this.logger.log("Bot protection rule", result); }
if (result.reason.isEmail()) { this.logger.log("Email validation rule", result); } }
if (decision.isDenied()) { if (decision.reason.isEmail()) { this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`);
let message: string;
// These are specific errors to help the user, but will also reveal the // validation to a spammer. if (decision.reason.emailTypes.includes("INVALID")) { message = "email address format is invalid. Is there a typo?"; } else if (decision.reason.emailTypes.includes("DISPOSABLE")) { message = "we do not allow disposable email addresses."; } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { message = "your email domain does not have an MX record. Is there a typo?"; } else { // This is a catch all, but the above should be exhaustive based on the // configured rules. message = "invalid email."; }
throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST); } else { throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); } }
return this.signupService.signup(body.email); }}Check the email type
Section titled “Check the email type”Arcjet returns the type of email address that it verified. This is one or several of the reported email types.
type ArcjetEmailType = | "DISPOSABLE" // Disposable email address from a throwaway email service | "FREE" // Email address from a free email service | "NO_MX_RECORDS" // Email address with no MX records i.e. is undeliverable | "NO_GRAVATAR" // Email address with no Gravatar profile | "INVALID"; // Email address that is invalidfrom arcjet import EmailType
# EmailType is a str Enum with the same values:# - EmailType.DISPOSABLE — Disposable email from a throwaway service# - EmailType.FREE — Email from a free provider (e.g. Gmail)# - EmailType.NO_MX_RECORDS — Domain has no MX records (undeliverable)# - EmailType.NO_GRAVATAR — No Gravatar profile# - EmailType.INVALID — Address fails syntax / format validationYou can check the email type using the decision.reason.emailTypes (JS) /
decision.reason_v2.email_types (Python) array:
let message = "";// You could return specific messages based on the email type, but this would// also reveal the validation to a spammerif (decision.reason.emailTypes.includes("DISPOSABLE")) { message = "We do not allow disposable email addresses.";} else if (decision.reason.emailTypes.includes("FREE")) { message = "We do not allow free email addresses, please use a business address.";} else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { message = "Your email domain does not have an MX record. Is there a typo?";} else if (decision.reason.emailTypes.includes("NO_GRAVATAR")) { message = "We require a Gravatar profile to sign up.";} else { // This is a catch all message = "Invalid email.";}message = ""email_types = decision.reason_v2.email_types # only valid when reason_v2.type == "EMAIL"
# You could return specific messages based on the email type, but this would# also reveal the validation to a spammerif "DISPOSABLE" in email_types: message = "We do not allow disposable email addresses."elif "FREE" in email_types: message = ( "We do not allow free email addresses, please use a business address." )elif "NO_MX_RECORDS" in email_types: message = "Your email domain does not have an MX record. Is there a typo?"elif "NO_GRAVATAR" in email_types: message = "We require a Gravatar profile to sign up."else: # This is a catch all message = "Invalid email."Error handling
Section titled “Error handling”Arcjet is designed to fail open so that a service issue or misconfiguration does
not block all requests. The SDK also times out and fails open after 1000 ms
in development (see ARCJET_ENV) and 500 ms
otherwise. However, in most cases, the response time is less than 20 ms to
30 ms.
If there is an error condition when processing the rule, Arcjet returns an
ERROR result for that rule and you can check the message property on the
rule’s error result for more information.
If all other rules that were run returned an ALLOW result, then the final
Arcjet conclusion is ERROR.
import loggingimport os
from arcjet import EmailType, Mode, arcjet, validate_emailfrom fastapi import FastAPI, Form, Requestfrom fastapi.responses import JSONResponse
app = FastAPI()
logger = logging.getLogger(__name__)
aj = arcjet( key=os.environ["ARCJET_KEY"], # Get your site key from https://app.arcjet.com rules=[ validate_email( mode=Mode.LIVE, # Blocks requests. Use Mode.DRY_RUN to log only deny=[EmailType.NO_MX_RECORDS], # block email addresses with no MX records ), ],)
@app.post("/")async def index(request: Request, email: str = Form(...)): logger.info("Email received: %s", email)
decision = await aj.protect(request, email=email) logger.info("Arcjet decision %s", decision)
for result in decision.results: if result.reason_v2.type == "ERROR": # Fail open by logging the error and continuing logger.warning("Arcjet error: %s", result.reason_v2.message) # You could also fail closed here for very sensitive routes: # return JSONResponse({"error": "Service unavailable"}, status_code=503)
if decision.is_denied(): if decision.reason_v2.type == "EMAIL": return JSONResponse({"error": "Invalid email"}, status_code=400) return JSONResponse({"error": "Forbidden"}, status_code=403)
return {"message": "Hello world", "email": email}import loggingimport os
from arcjet import EmailType, Mode, arcjet_sync, validate_emailfrom flask import Flask, jsonify, request
app = Flask(__name__)
logger = logging.getLogger(__name__)
aj = arcjet_sync( key=os.environ["ARCJET_KEY"], # Get your site key from https://app.arcjet.com rules=[ validate_email( mode=Mode.LIVE, # Blocks requests. Use Mode.DRY_RUN to log only deny=[EmailType.NO_MX_RECORDS], # block email addresses with no MX records ), ],)
@app.post("/")def index(): email = request.form.get("email", "") logger.info("Email received: %s", email)
decision = aj.protect(request, email=email) logger.info("Arcjet decision %s", decision)
for result in decision.results: if result.reason_v2.type == "ERROR": # Fail open by logging the error and continuing logger.warning("Arcjet error: %s", result.reason_v2.message) # You could also fail closed here for very sensitive routes: # return jsonify(error="Service unavailable"), 503
if decision.is_denied(): if decision.reason_v2.type == "EMAIL": return jsonify(error="Invalid email"), 400 return jsonify(error="Forbidden"), 403
return jsonify(message="Hello world", email=email)import arcjet, { validateEmail } from "@arcjet/bun";import { env } from "bun";
const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), ],});
export default { port: 3000, fetch: aj.handler(async (req) => { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes // return new Response("Service unavailable", { status: 503 }); } }
if (decision.isDenied()) { return new Response("Forbidden", { status: 403 }); }
return new Response("Hello world"); }),};import arcjet, { validateEmail } from "@arcjet/bun";import { env } from "bun";
const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), ],});
export default { port: 3000, fetch: aj.handler(async (req) => { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes // return new Response("Service unavailable", { status: 503 }); } }
if (decision.isDenied()) { return new Response("Forbidden", { status: 403 }); }
return new Response("Hello world"); }),};import arcjet, { validateEmail } from "@arcjet/next";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), ],});
export default async function handler(req, res) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return res.status(503).json({ error: "Service unavailable" }); } }
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}import arcjet, { validateEmail } from "@arcjet/next";import type { NextApiRequest, NextApiResponse } from "next";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), ],});
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return res.status(503).json({ error: "Service unavailable" }); } }
if (decision.isDenied()) { return res .status(403) .json({ error: "Forbidden", reason: decision.reason }); }
res.status(200).json({ name: "Hello world" });}import arcjet, { validateEmail } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), ],});
export async function POST(req: Request) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return NextResponse.json({ error: "Service unavailable" }, { status: 503 }); } }
if (decision.isDenied()) { return NextResponse.json( { error: "Forbidden", }, { status: 403, }, ); }
return NextResponse.json({ message: "Hello world", });}import arcjet, { validateEmail } from "@arcjet/next";import { NextResponse } from "next/server";
const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), ],});
export async function POST(req) { const decision = await aj.protect(req, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return NextResponse.json({ error: "Service unavailable" }, { status: 503 }); } }
if (decision.isDenied()) { return NextResponse.json( { error: "Forbidden", }, { status: 403, }, ); }
return NextResponse.json({ message: "Hello world", });}import { env } from "$env/dynamic/private";import arcjet, { validateEmail } from "@arcjet/sveltekit";import { error, json, type RequestEvent } from "@sveltejs/kit";
const aj = arcjet({ key: env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), ],});
export async function POST(event: RequestEvent) { const decision = await aj.protect(event, { // The email prop is required when a validateEmail rule is configured. // TypeScript will guide you based on the configured rules email: "test@0zc7eznv3rsiswlohu.tk", });
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return error(503, { message: "Service unavailable" }); } }
if (decision.isDenied()) { return error(403, { message: "Forbidden" }); }
return json({ message: "Hello world" });}import { env } from "$env/dynamic/private";import arcjet, { validateEmail } from "@arcjet/sveltekit";import { error, json } from "@sveltejs/kit";
const aj = arcjet({ key: env.ARCJET_KEY, // Get your site key from https://app.arcjet.com rules: [ validateEmail({ mode: "LIVE", deny: ["DISPOSABLE"], }), ],});
export async function POST(event) { const decision = await aj.protect(event, { // The email prop is required when a validateEmail rule is configured. email: "test@0zc7eznv3rsiswlohu.tk", });
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //return error(503, { message: "Service unavailable" }); } }
if (decision.isDenied()) { return error(403, { message: "Forbidden" }); }
return json({ message: "Hello world" });}import arcjet, { validateEmail } from "@arcjet/node";import express from "express";
const app = express();const port = 3000;
app.use(express.urlencoded({ extended: false }));
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only deny: ["NO_MX_RECORDS"], // block email addresses with no MX records }), ],});
app.post("/", async (req, res) => { console.log("Email received: ", req.body.email);
const decision = await aj.protect(req, { email: req.body.email, }); console.log("Arcjet decision", decision);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //res.writeHead(503, { "Content-Type": "application/json" }); //res.end(JSON.stringify({ error: "Service unavailable" })); } }
if (decision.isDenied()) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Forbidden" })); } else { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello World", email: req.body.email })); }});
app.listen(port, () => { console.log(`Example app listening on port ${port}`);});import arcjet, { validateEmail } from "@arcjet/node";import express from "express";
const app = express();const port = 3000;
app.use(express.urlencoded({ extended: false }));
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only deny: ["NO_MX_RECORDS"], // block email addresses with no MX records }), ],});
app.post("/", async (req, res) => { console.log("Email received: ", req.body.email);
const decision = await aj.protect(req, { email: req.body.email, }); console.log("Arcjet decision", decision);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes //res.writeHead(503, { "Content-Type": "application/json" }); //res.end(JSON.stringify({ error: "Service unavailable" })); } }
if (decision.isDenied()) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Forbidden" })); } else { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello World", email: req.body.email })); }});
app.listen(port, () => { console.log(`Example app listening on port ${port}`);});import arcjet, { validateEmail } from "@arcjet/remix";import type { ActionFunctionArgs } from "@remix-run/node";
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only // block disposable, invalid, and email addresses with no MX records deny: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], }), ],});
// The action function is called for non-GET requests, which is where you// typically handle form submissions that might contain an email address.export async function action(args: ActionFunctionArgs) { // The request body is a FormData object const formData = await args.request.formData(); const email = formData.get("email") as string;
const decision = await aj.protect(args, { email }); console.log("Arcjet decision", decision);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes // throw new Response("Service unavailable", { status: 503, statusText: "Service unavailable" }); } }
if (decision.isDenied()) { if (decision.reason.isEmail()) { return Response.json({ error: "Invalid email." }, { status: 400 }); } else { return Response.json({ error: "Forbidden" }, { status: 403 }); } }
// We don't need to use the decision elsewhere, but you could return it to // the component return null;}import arcjet, { validateEmail } from "@arcjet/remix";
const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment // variable rather than hard coding. key: process.env.ARCJET_KEY, rules: [ validateEmail({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only // block disposable, invalid, and email addresses with no MX records deny: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], }), ],});
// The action function is called for non-GET requests, which is where you// typically handle form submissions that might contain an email address.export async function action(args) { // The request body is a FormData object const formData = await args.request.formData(); const email = formData.get("email");
const decision = await aj.protect(args, { email }); console.log("Arcjet decision", decision);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open by logging the error and continuing console.warn("Arcjet error", reason.message); // You could also fail closed here for very sensitive routes // throw new Response("Service unavailable", { status: 503, statusText: "Service unavailable" }); } }
if (decision.isDenied()) { if (decision.reason.isEmail()) { return Response.json({ error: "Invalid email." }, { status: 400 }); } else { return Response.json({ error: "Forbidden" }, { status: 403 }); } }
// We don't need to use the decision elsewhere, but you could return it to // the component return null;}import { ARCJET, type ArcjetNest, validateEmail } from "@arcjet/nest";import { isMissingUserAgent } from "@arcjet/inspect";import { Body, Controller, HttpException, HttpStatus, Inject, Injectable, Logger, Post, Req, UseInterceptors,} from "@nestjs/common";import { NoFilesInterceptor } from "@nestjs/platform-express";import { IsNotEmpty } from "class-validator";import type { Request } from "express";
// Validation class as described at// https://docs.nestjs.com/techniques/validation. We're not using the IsEmail// decorator here because Arcjet handles this for you.export class SignupDto { @IsNotEmpty() // @ts-ignore: This is a DTO class so ignore that it's not definitely assigned email: string;}
// This would normally go in your service file e.g.// src/signup/signup.service.ts@Injectable()export class SignupService { private readonly logger = new Logger(SignupService.name);
signup(email: string): { message: string } { this.logger.log(`Form submission: ${email}`);
return { message: "Hello world", }; }}
// This would normally go in your controller file e.g.// src/signup/signup.controller.ts@Controller("signup")export class SignupController { private readonly logger = new Logger(SignupController.name);
constructor( private readonly signupService: SignupService, @Inject(ARCJET) private readonly arcjet: ArcjetNest, ) {}
// Implement a form handler following // https://docs.nestjs.com/techniques/file-upload#no-files. Note this isn't // compatible with the NestJS Fastify adapter. @Post() @UseInterceptors(NoFilesInterceptor()) async index(@Req() req: Request, @Body() body: SignupDto) { const decision = await this.arcjet .withRule( validateEmail({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only // block disposable, invalid, and email addresses with no MX records deny: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"], }), ) .protect(req, { email: body.email });
this.logger.log(`Arcjet: id = ${decision.id}`); this.logger.log(`Arcjet: decision = ${decision.conclusion}`);
for (const { reason } of decision.results) { if (reason.isError()) { // Fail open to prevent an Arcjet error from blocking all requests. You // may want to fail closed if this controller is very sensitive this.logger.error(`Arcjet error: ${reason.message}`); // You could also fail closed here for very sensitive routes // throw new HttpException("Service unavailable", HttpStatus.SERVICE_UNAVAILABLE); } }
if (decision.isDenied()) { if (decision.reason.isEmail()) { this.logger.log(`Arcjet: email error = ${decision.reason.emailTypes}`);
let message: string;
// These are specific errors to help the user, but will also reveal the // validation to a spammer. if (decision.reason.emailTypes.includes("INVALID")) { message = "email address format is invalid. Is there a typo?"; } else if (decision.reason.emailTypes.includes("DISPOSABLE")) { message = "we do not allow disposable email addresses."; } else if (decision.reason.emailTypes.includes("NO_MX_RECORDS")) { message = "your email domain does not have an MX record. Is there a typo?"; } else { // This is a catch all, but the above should be exhaustive based on the // configured rules. message = "invalid email."; }
throw new HttpException(`Error: ${message}`, HttpStatus.BAD_REQUEST); } else { throw new HttpException("Forbidden", HttpStatus.FORBIDDEN); } }
if (decision.results.some(isMissingUserAgent)) { // Requests without User-Agent headers might not be identified as any // particular bot and could be marked as an errored result. Most // legitimate clients send this header, so we recommend blocking requests // without it. // See https://docs.arcjet.com/bot-protection/reference#user-agent-header this.logger.warn("User-Agent header is missing"); throw new HttpException("Bad request", HttpStatus.BAD_REQUEST); }
return this.signupService.signup(body.email); }}Testing
Section titled “Testing”Arcjet runs the same in any environment, including locally and in CI. You can
use the mode set to DRY_RUN to log the results of rule execution without
blocking any requests.
We have an example test framework you can use to automatically test your rules. Arcjet can also be triggered based using a sample of your traffic.
For details, see the Testing section of the docs.
RFC reference
Section titled “RFC reference”The email validation stage considers the following RFCs:
- RFC 1123: Requirements for Internet Hosts — Application and Support, IETF, Oct 1989.
- RFC 3629: UTF-8, a transformation format of ISO 10646, IETF, Nov 2003.
- RFC 3696: Application Techniques for Checking and Transformation of Names, IETF, Feb 2004.
- RFC 4291: IP Version 6 Addressing Architecture, IETF, Feb 2006.
- RFC 5234: Augmented BNF for Syntax Specifications: ABNF, IETF, Jan 2008.
- RFC 5321: Simple Mail Transfer Protocol, IETF, Oct 2008.
- RFC 5322: Internet Message Format, I ETF, Oct 2008.
- RFC 5890: Internationalized Domain Names for Applications (IDNA): Definitions and Document Framework, IETF, Aug 2010.
- RFC 6531: SMTP Extension for Internationalized Email, IETF, Feb 2012.
- RFC 6532: Internationalized Email Headers, IETF, Feb 2012.