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.
In JavaScript / TypeScript these are constructed with the
validateEmail(options: EmailOptions) function. In Python they are constructed
with the validate_email(...) factory.
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";# Signature for arcjet.validate_email# Pass exactly one of `allow` or `deny`. Passing neither or both raises# ValueError. An empty allow=[] allows no email types.def validate_email( *, # Required. Mode.LIVE blocks requests; Mode.DRY_RUN logs only. mode: Mode, # Email types to reject. All other types are allowed. deny: Sequence[str | EmailType] | None = None, # Email types to permit. All other types are rejected. allow: Sequence[str | EmailType] | None = None, # Reject addresses without a valid TLD. Defaults to True. require_top_level_domain: bool = True, # Allow IP-literal domains such as user@[192.0.2.1]. Defaults to False. allow_domain_literal: bool = False,) -> EmailValidation: ...You must pass exactly one of allow or deny. The 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.
An empty allow list is valid and allows no email types. In Python,
validate_email and the EmailValidation dataclass raise ValueError if you
pass neither list or both lists.
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.
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.
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://console.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}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://console.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}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 2000 ms by default. 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://console.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}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.