Email Validator — Java source
Validate email addresses one at a time or in bulk. Checks syntax, length limits, local-part and domain rules, plus-addressing, and IP-literal domains - all in your browser.
This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// email-validator — RFC 5321/5322-inspired email validation.
//
// Language: Java (Java 17, standard library only)
// Source: CosmoDev polyglot showcase port of the Email Validator tool,
// ported from src/lib/email-validator.ts (the canonical TypeScript
// implementation).
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Practical, provider-friendly validation: errs on the side of deliverability
// while still recognising the legal-but-unusual forms (quoted local parts,
// IP-literal domains). Pure and deterministic — every malformed input becomes
// a non-valid verdict carrying explanatory reasons; nothing below throws.
//
// Self-contained: the character classes used by the original are implemented
// as small char predicates, avoiding java.util.regex.
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
/** Pure logic of the Email Validator tool. All methods are static and side-effect free. */
public final class EmailValidator {
private EmailValidator() {}
/** RFC-inspired length ceilings: local part, domain, total address. */
static final int LOCAL_MAX = 64;
static final int DOMAIN_MAX = 253;
static final int TOTAL_MAX = 320;
private static final String WHITESPACE = " \t\n\r\f";
/** The structured verdict returned by {@link #validateEmail}. */
public static final class EmailResult {
/** True when no blocking reasons were recorded. */
public final boolean valid;
/** Local part (before '@'); empty when not parseable. */
public final String local;
/** Domain part (after '@'); empty when not parseable. */
public final String domain;
/** {@code local@lowercased-domain} when both parts exist, else empty. */
public final Optional<String> normalized;
/** Blocking problems ({@code valid} is true iff this is empty). */
public final List<String> reasons;
/** Non-blocking observations (rare forms, plus-tags, ...). */
public final List<String> warnings;
EmailResult(boolean valid, String local, String domain, Optional<String> normalized,
List<String> reasons, List<String> warnings) {
this.valid = valid;
this.local = local;
this.domain = domain;
this.normalized = normalized;
this.reasons = reasons;
this.warnings = warnings;
}
}
/** Internal split result: views into the input address. */
private record Split(String local, String domain, boolean quoted) {}
// ------------------------------------------------------------ predicates ---
/** True when every char of {@code s} belongs to the RFC-style "atom"
* character set (ASCII alphanumeric plus the printable specials
* permitted unquoted). */
private static boolean isAtomLocal(String s) {
if (s.isEmpty()) return false;
for (int i = 0; i < s.length(); i++) {
if (!isAtomChar(s.charAt(i))) return false;
}
return true;
}
private static boolean isAtomChar(char c) {
if (isAsciiAlphanumeric(c)) return true;
return ".!#$%&'*+/=?^_`{|}~-".indexOf(c) >= 0;
}
/** A valid domain label: ASCII letters, digits, and hyphens (non-empty). */
private static boolean isValidLabel(String s) {
if (s.isEmpty()) return false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (!isAsciiAlphanumeric(c) && c != '-') return false;
}
return true;
}
/** A valid TLD: two or more ASCII letters. Length equals char count when
* every char is ASCII alphabetic, so the length check is exact. */
private static boolean isValidTld(String s) {
if (s.length() < 2) return false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) return false;
}
return true;
}
/** An all-decimal, non-empty octet string. */
private static boolean isDecimal(String s) {
if (s.isEmpty()) return false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c < '0' || c > '9') return false;
}
return true;
}
/** True when {@code s} starts with "ipv6:" (case-insensitive). */
private static boolean isIpv6Literal(String s) {
return s.length() >= 5 && s.substring(0, 5).toLowerCase(Locale.ROOT).equals("ipv6:");
}
/** True when {@code s} is a dotted-quad: four octets, each 0-255, with no
* leading zeros. The 3-digit length cap rejects arbitrarily long digit
* strings before they can overflow the value parse (equivalent to the
* reference's overflow-on-cast behaviour). */
private static boolean isIpv4(String s) {
String[] parts = s.split("\\.", -1);
if (parts.length != 4) return false;
for (String p : parts) {
if (p.isEmpty() || p.length() > 3 || !isDecimal(p)) return false;
int value = Integer.parseInt(p);
if (value > 255) return false;
if (p.length() > 1 && p.charAt(0) == '0') return false; // leading zero ("01")
}
return true;
}
private static boolean isAsciiAlphanumeric(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
}
// ----------------------------------------------------------------- split ---
/** Splits an address into local + domain, honouring a quoted ("...")
* local part. Returns empty when the address cannot be split into
* exactly one '@' in the right place. */
private static Optional<Split> splitLocalDomain(String email) {
if (email.startsWith("\"")) {
// Walk the quoted string; a backslash escapes the next char (so
// `\"` does not terminate the quote).
int i = 1;
int n = email.length();
while (i < n) {
char ch = email.charAt(i);
if (ch == '\\') { i += 2; continue; }
if (ch == '"') break;
i++;
}
if (i >= n || email.charAt(i) != '"') return Optional.empty(); // unterminated quote
int at = i + 1;
if (at >= n || email.charAt(at) != '@') return Optional.empty(); // '@' must follow quote
if (email.indexOf('@', at + 1) != -1) return Optional.empty(); // stray '@' in the domain
return Optional.of(new Split(email.substring(0, at), email.substring(at + 1), true));
}
int first = email.indexOf('@');
if (first == -1) return Optional.empty();
if (email.indexOf('@', first + 1) != -1) return Optional.empty(); // multiple '@'
return Optional.of(new Split(email.substring(0, first), email.substring(first + 1), false));
}
// ---------------------------------------------------------------- domain ---
/** Appends domain-level problems to {@code reasons} / {@code warnings}. */
private static void validateDomain(String domain, List<String> reasons, List<String> warnings) {
if (domain.isEmpty()) {
reasons.add("Domain is empty");
return;
}
if (domain.length() > DOMAIN_MAX) {
reasons.add("Domain exceeds " + DOMAIN_MAX + " characters");
}
// IP-literal domain: [1.2.3.4] or [IPv6:...].
if (domain.startsWith("[") && domain.endsWith("]")) {
String inner = domain.substring(1, domain.length() - 1);
if (isIpv6Literal(inner)) {
warnings.add("IPv6 literal domain (uncommon; ensure your provider supports it)");
return;
}
if (isIpv4(inner)) {
warnings.add("IP-literal domain (uncommon; ensure your provider supports it)");
return;
}
reasons.add("Invalid IP-literal domain");
return;
}
if (domain.startsWith("[") || domain.endsWith("]")) {
reasons.add("Malformed IP-literal domain (unmatched brackets)");
return;
}
if (!domain.contains(".")) {
reasons.add("Domain must contain at least one dot (e.g. example.com)");
return;
}
String[] labels = domain.split("\\.", -1);
for (String label : labels) {
if (label.isEmpty()) {
reasons.add("Domain contains an empty label (consecutive or trailing dots)");
continue;
}
if (label.length() > 63) {
reasons.add("Domain label exceeds 63 characters");
}
if (!isValidLabel(label)) {
reasons.add("Domain label contains invalid characters");
}
if (label.startsWith("-") || label.endsWith("-")) {
reasons.add("Domain label starts or ends with a hyphen");
}
}
// The TLD is the final label; require >=2 ASCII letters so bare
// hostnames and numeric tails are rejected.
String tld = labels[labels.length - 1];
if (!isValidTld(tld)) {
reasons.add("Top-level domain must be at least two letters");
}
}
// ----------------------------------------------------------------- email ---
/** Validates a single email address, returning a structured verdict.
* Pure and deterministic: every malformed input becomes a non-valid
* result carrying explanatory reasons. */
public static EmailResult validateEmail(String raw) {
List<String> reasons = new ArrayList<>();
List<String> warnings = new ArrayList<>();
String email = trim(raw);
if (email.isEmpty()) {
reasons.add("Email is empty");
return new EmailResult(false, "", "", Optional.empty(), reasons, warnings);
}
if (email.length() > TOTAL_MAX) {
reasons.add("Email exceeds maximum length of " + TOTAL_MAX + " characters");
}
Optional<Split> split = splitLocalDomain(email);
if (split.isEmpty()) {
reasons.add("Email must contain exactly one \"@\" separating local part and domain");
return new EmailResult(false, "", "", Optional.empty(), reasons, warnings);
}
String local = split.get().local();
String domain = split.get().domain();
boolean quoted = split.get().quoted();
if (quoted) {
// Quoted local parts are RFC-legal but almost universally rejected
// by mailbox providers — warn, and only length-check structurally.
if (local.length() > LOCAL_MAX) {
reasons.add("Local part exceeds " + LOCAL_MAX + " characters");
}
warnings.add("Quoted local part (rarely supported by providers)");
} else if (local.isEmpty()) {
reasons.add("Local part is empty");
} else {
if (local.length() > LOCAL_MAX) {
reasons.add("Local part exceeds " + LOCAL_MAX + " characters");
}
if (local.startsWith(".") || local.endsWith(".")) {
reasons.add("Local part starts or ends with a dot");
}
if (local.contains("..")) {
reasons.add("Local part contains consecutive dots");
}
if (!isAtomLocal(local)) {
reasons.add("Local part contains invalid characters");
}
}
// Plus-addressing (`user+tag@`) is valid and delivers to the base
// mailbox, but callers filtering on exact address may want to know.
if (!quoted && local.contains("+")) {
warnings.add("Plus-addressing (tag) detected — delivers to the base mailbox");
}
validateDomain(domain, reasons, warnings);
boolean valid = reasons.isEmpty();
Optional<String> normalized = (!local.isEmpty() && !domain.isEmpty())
? Optional.of(local + "@" + domain.toLowerCase(Locale.ROOT))
: Optional.empty();
return new EmailResult(valid, local, domain, normalized, reasons, warnings);
}
/** Validates many addresses — one per line. Blank or whitespace-only lines
* are skipped. Line endings may be LF or CRLF (matching the reference's
* {@code \r?\n} split). */
public static List<EmailResult> validateBatch(String text) {
List<EmailResult> out = new ArrayList<>();
if (text.isEmpty()) return out;
for (String line : text.split("\\r?\\n", -1)) {
String trimmed = trim(line);
if (!trimmed.isEmpty()) {
out.add(validateEmail(trimmed));
}
}
return out;
}
/** Strip ASCII leading/trailing whitespace (String.trim() is Unicode-aware;
* the reference trims a fixed whitespace set, so mirror that exactly). */
private static String trim(String s) {
int b = 0, e = s.length();
while (b < e && WHITESPACE.indexOf(s.charAt(b)) >= 0) b++;
while (e > b && WHITESPACE.indexOf(s.charAt(e - 1)) >= 0) e--;
return s.substring(b, e);
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →