Skip to content

Instantly share code, notes, and snippets.

@AndrewDongminYoo
Created April 12, 2025 11:15
Show Gist options
  • Save AndrewDongminYoo/27780785e90e4af78b0fea1572d1abde to your computer and use it in GitHub Desktop.
Save AndrewDongminYoo/27780785e90e4af78b0fea1572d1abde to your computer and use it in GitHub Desktop.
Dart class that uses a strategy pattern to check the validity of an email
/// Written with “powerful” inspiration from the https://pub.dev/packages/email_validator.
/// An abstract strategy for validating objects of type [T].
///
/// Implementations of this class define specific validation rules
/// for a given type, allowing flexible and reusable validation logic.
abstract class ValidationStrategy<T, E extends Exception> {
/// Validates the given [value] of type [T].
///
/// If the validation fails, an exception of type [E] is thrown.
void call(T value);
}
// Maximum overall email length (less than 255 characters)
const int kMaxEmailLength = 254;
// Maximum length for the local-part
const int kMaxLocalPartLength = 64;
// Maximum length for each domain component
const int kMaxDomainComponentLength = 64;
// Minimum required length for IPv4/IPv6 literal
const int kMinIpLiteralLength = 8;
/// Email validation strategy class.
/// Throws [EmailValidationException] if the email is invalid.
class EmailValidationStrategy implements ValidationStrategy<String, EmailValidationException> {
EmailValidationStrategy({
this.allowTopLevelDomains = false,
this.allowInternational = true,
this.debug = false,
});
/// Whether top-level domains are allowed.
final bool allowTopLevelDomains;
/// Whether international email addresses are allowed.
final bool allowInternational;
/// Whether the parser is in debug mode.
final bool debug;
@override
void call(String email) {
final parser = _EmailValidatorParser(email, allowTopLevelDomains, allowInternational, debug: debug);
if (!parser.validate()) {
// In debug mode, detailed error messages are available via parser.errorMessage
throw EmailValidationException(parser.errorMessage ?? 'Invalid email address.');
}
}
}
/// Exception thrown when email validation fails.
class EmailValidationException implements Exception {
EmailValidationException(this.message);
/// The error message associated with the validation failure.
final String message;
@override
String toString() {
return 'EmailValidationException: $message';
}
}
/// Enumeration for subdomain types: [none], [alphabetic], [numeric], [alphanumeric]
enum SubdomainType { none, alphabetic, numeric, alphanumeric }
/// Internal implementation of email validation using a parser.
/// Each validation step relies on state ([_position], [_domainType]) ensuring thread safety.
/// When debug mode is enabled, detailed failure reasons are recorded.
class _EmailValidatorParser {
_EmailValidatorParser(this.email, this.allowTopLevelDomains, this.allowInternational, {this.debug = false});
/// The email address being validated.
final String email;
/// Whether top-level domains are allowed.
final bool allowTopLevelDomains;
/// Whether international email addresses are allowed.
final bool allowInternational;
/// Whether the parser is in debug mode.
final bool debug;
/// The current position in the email address.
int _position = 0;
/// The type of the current domain.
SubdomainType _domainType = SubdomainType.none;
/// The error message, if any.
String? _errorMessage;
/// Helper function that sets an error message and returns false upon failure.
bool _fail(String message) {
if (debug && _errorMessage == null) {
_errorMessage = message;
}
return false;
}
/// Provides the first failure reason when debug mode is active.
String? get errorMessage => _errorMessage;
/// Checks if the character [char] is a digit.
bool _isDigit(String char) {
final code = char.codeUnitAt(0);
return code >= 48 && code <= 57;
}
/// Checks if the character [char] is an alphabetic letter.
bool _isLetter(String char) {
final code = char.codeUnitAt(0);
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
}
/// Checks if the character [char] is either a letter or a digit.
bool _isLetterOrDigit(String char) {
return _isLetter(char) || _isDigit(char);
}
/// Determines if the character [char] is allowed in the local-part atom.
bool _isAtomChar(String char) {
const atomCharacters = r"!#$%&'*+-/=?^_`{|}~";
if (char.codeUnitAt(0) < 128) {
return _isLetterOrDigit(char) || atomCharacters.contains(char);
}
return allowInternational;
}
/// Checks domain characters while updating [_domainType].
bool _isDomainChar(String char) {
if (char.codeUnitAt(0) < 128) {
if (_isLetter(char) || char == '-') {
_domainType = SubdomainType.alphabetic;
return true;
}
if (_isDigit(char)) {
_domainType = SubdomainType.numeric;
return true;
}
return false;
}
if (allowInternational) {
_domainType = SubdomainType.alphabetic;
return true;
}
return false;
}
/// Checks the starting character of a domain.
bool _isDomainStartChar(String char) {
if (char.codeUnitAt(0) < 128) {
if (_isLetter(char)) {
_domainType = SubdomainType.alphabetic;
return true;
}
if (_isDigit(char)) {
_domainType = SubdomainType.numeric;
return true;
}
_domainType = SubdomainType.none;
return false;
}
if (allowInternational) {
_domainType = SubdomainType.alphabetic;
return true;
}
_domainType = SubdomainType.none;
return false;
}
/// Parses the atom part of the local-part.
bool _skipAtom() {
final start = _position;
while (_position < email.length && _isAtomChar(email[_position])) {
_position++;
}
if (_position == start) {
return _fail('Failed to parse local-part atom at position $_position');
}
if (_position > kMaxLocalPartLength) {
return _fail('Local-part exceeds maximum length of $kMaxLocalPartLength characters');
}
return true;
}
/// Parses a single subdomain.
bool _skipSubDomain() {
final start = _position;
if (_position >= email.length || !_isDomainStartChar(email[_position])) {
return _fail('Failed domain start character validation at position $_position');
}
// Consume the starting domain character
_position++;
while (_position < email.length && _isDomainChar(email[_position])) {
_position++;
}
// A single-character TLD is not valid.
if (_position == email.length && (_position - start) == 1) {
return _fail('Single-character domain is not valid at position $start');
}
if ((_position - start) >= kMaxDomainComponentLength) {
return _fail('Domain component exceeds maximum length of $kMaxDomainComponentLength characters');
}
if (email[_position - 1] == '-') {
return _fail('Domain cannot end with "-" at position ${_position - 1}');
}
return true;
}
/// Parses the entire domain (subdomains and top-level domain).
bool _skipDomain() {
if (!_skipSubDomain()) {
return false;
}
if (_position < email.length && email[_position] == '.') {
do {
// Skip the dot ('.')
_position++;
if (_position >= email.length) {
return _fail('Insufficient content after dot in domain parsing');
}
if (!_skipSubDomain()) {
return false;
}
} while (_position < email.length && email[_position] == '.');
} else if (!allowTopLevelDomains) {
return _fail('Top-level domains are not allowed');
}
if (_domainType == SubdomainType.numeric) {
return _fail('Numeric domains are not valid');
}
return true;
}
/// Parses the content within a quoted string.
bool _skipQuoted() {
var escaped = false;
// Skip the starting double quote
_position++;
while (_position < email.length) {
if (email[_position].codeUnitAt(0) >= 128 && !allowInternational) {
return _fail('International characters are not allowed in quoted string at position $_position');
}
if (email[_position] == r'\') {
escaped = !escaped;
} else if (!escaped) {
if (email[_position] == '"') {
break;
}
} else {
escaped = false;
}
_position++;
}
if (_position >= email.length || email[_position] != '"') {
return _fail('Missing ending double quote in quoted string');
}
// Skip the ending double quote
_position++;
return true;
}
/// Parses an IPv4 literal.
bool _skipIPv4Literal() {
var groups = 0;
while (_position < email.length && groups < 4) {
final groupStart = _position;
var value = 0;
while (_position < email.length && _isDigit(email[_position])) {
value = (value * 10) + (email[_position].codeUnitAt(0) - 48);
_position++;
}
if (_position == groupStart || (_position - groupStart) > 3 || value > 255) {
return _fail('Failed to parse IPv4 group starting at $groupStart');
}
groups++;
if (groups < 4) {
if (_position < email.length && email[_position] == '.') {
// Skip the dot ('.')
_position++;
} else {
return _fail('Missing IPv4 group delimiter (.) at position $_position');
}
}
}
if (groups != 4) {
return _fail('IPv4 address does not have 4 groups');
}
return true;
}
/// Checks if the character [char] is a hexadecimal digit.
bool _isHexDigit(String char) {
final code = char.codeUnitAt(0);
return (code >= 65 && code <= 70) || (code >= 97 && code <= 102) || (code >= 48 && code <= 57);
}
/// Parses an IPv6 literal.
bool _skipIPv6Literal() {
var compact = false;
var colons = 0;
while (_position < email.length) {
final segmentStart = _position;
while (_position < email.length && _isHexDigit(email[_position])) {
_position++;
}
if (_position >= email.length) {
break;
}
// Handle mixed IPv6/IPv4: if enough hex digits are followed by '.', switch to IPv4 parsing
if (_position > segmentStart && colons > 2 && email[_position] == '.') {
_position = segmentStart;
if (!_skipIPv4Literal()) {
return _fail('Failed to parse mixed IPv4 within IPv6');
}
return compact ? colons < 6 : colons == 6;
}
final count = _position - segmentStart;
if (count > 4) {
return _fail('IPv6 segment exceeds 4 characters at $segmentStart');
}
if (email[_position] != ':') {
break;
}
final colonStart = _position;
while (_position < email.length && email[_position] == ':') {
_position++;
}
final colonCount = _position - colonStart;
if (colonCount > 2) {
return _fail('Too many consecutive colons (:) starting at $colonStart');
}
if (colonCount == 2) {
if (compact) {
return _fail('Multiple uses of compression (::) at $colonStart');
}
compact = true;
colons += 2;
} else {
colons++;
}
}
if (colons < 2) {
return _fail('Insufficient number of colons in IPv6 address');
}
return compact ? colons < 7 : colons == 7;
}
/// Performs full email validation.
bool validate() {
// Check if the email is empty or exceeds the maximum length.
if (email.isEmpty || email.length >= kMaxEmailLength) {
return _fail('Email is empty or exceeds $kMaxEmailLength characters');
}
// Parse the local-part: if it starts with a double quote, treat it as a quoted string.
if (email[_position] == '"') {
if (!_skipQuoted() || _position >= email.length) {
return _fail('Failed to parse quoted string in local-part');
}
} else {
if (!_skipAtom()) {
return _fail('Failed to parse local-part atom');
}
while (_position < email.length && email[_position] == '.') {
// Skip the dot ('.')
_position++;
if (_position >= email.length || !_skipAtom()) {
return _fail('Failed to parse local-part segment after dot');
}
}
}
// Validate the '@' delimiter and local-part length.
if (_position + 1 >= email.length || _position > kMaxLocalPartLength || email[_position] != '@') {
return _fail('Failed to validate the local-part and domain delimiter (@) at position $_position');
}
// Skip the '@' character.
_position++;
if (_position >= email.length) {
return _fail('Insufficient characters for domain parsing');
}
if (email[_position] != '[') {
// Parse a standard domain.
if (!_skipDomain()) {
return false;
}
return _position == email.length;
}
// Parse the address literal ([...])
// Skip the '[' character.
_position++;
if (_position + kMinIpLiteralLength >= email.length) {
return _fail('IP literal length is insufficient');
}
final literal = email.substring(_position - 1).toLowerCase();
if (literal.contains('ipv6:')) {
_position += 'ipv6:'.length;
if (!_skipIPv6Literal()) {
return false;
}
} else {
if (!_skipIPv4Literal()) {
return false;
}
}
if (_position >= email.length || email[_position] != ']') {
return _fail("Missing closing bracket (']') for IP literal");
}
// Skip the closing bracket.
_position++;
return _position == email.length;
}
}
@AndrewDongminYoo
Copy link
Author

Thank you for your warm response, @fredeil

@fredeil
Copy link

fredeil commented Oct 29, 2025

Could you link the blog post here? :) @AndrewDongminYoo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment