/** * Email Value Object * * Represents a validated email address. * Value objects are immutable and enforce validation rules. * * Domain invariants enforced: * - Email must be valid format * - Email is normalized (lowercase, trimmed) * * @layer Domain */ /** * Email Value Object * * Immutable representation of a validated email address. */ export class Email { private readonly value: string; private constructor(email: string) { this.value = this.normalize(email); this.validate(); } /** * Creates a new Email instance * * @param email - Email address string * @returns Email value object * @throws Error if email is invalid */ static create(email: string): Email { return new Email(email); } /** * Normalizes email address * * @param email - Raw email string * @returns Normalized email (lowercase, trimmed) */ private normalize(email: string): string { return email.toLowerCase().trim(); } /** * Validates email format * * @throws Error if email is invalid */ private validate(): void { if (!this.value || this.value.length === 0) { throw new Error('Email cannot be empty'); } // Basic email format validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(this.value)) { throw new Error(`Invalid email format: ${this.value}`); } // Additional validations if (this.value.length > 254) { throw new Error('Email too long (max 254 characters)'); } const [localPart, domain] = this.value.split('@'); if (localPart.length > 64) { throw new Error('Email local part too long (max 64 characters)'); } if (domain.length > 253) { throw new Error('Email domain too long (max 253 characters)'); } } /** * Gets email address string * * @returns Email address */ getValue(): string { return this.value; } /** * Gets domain part of email * * @returns Domain (e.g., "example.com" from "user@example.com") */ getDomain(): string { return this.value.split('@')[1]; } /** * Gets local part of email * * @returns Local part (e.g., "user" from "user@example.com") */ getLocalPart(): string { return this.value.split('@')[0]; } /** * Checks if email belongs to a specific domain * * @param domain - Domain to check * @returns true if email domain matches */ hasDomain(domain: string): boolean { return this.getDomain() === domain.toLowerCase(); } /** * Checks equality with another Email * * @param other - Email to compare * @returns true if emails are equal */ equals(other: Email): boolean { return this.value === other.value; } /** * Converts to string * * @returns Email address string */ toString(): string { return this.value; } /** * Converts to JSON * * @returns Email address string */ toJSON(): string { return this.value; } }