Skip to content

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 →