This release is 4 versions behind 0.4.0 — the latest version of @upyo/core. Jump to latest
Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
Works with
•JSR Score100%•This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers




Downloads390/wk
•Published2 months ago (0.3.1)
Simple email sending library for Node.js, Deno, Bun, and edge functions
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241/** * A type alias for email address strings that must contain an @ symbol. * @since 0.2.0 */ export type EmailAddress = `${string}@${string}`; /** * A pair of name (which is optional) and email address. */ export interface Address { /** * The name of the person or entity associated with the email address. */ readonly name?: string; /** * The email address itself. */ readonly address: EmailAddress; } /** * Formats an address object into a string representation. This function is * an inverse of the {@link parseAddress} function. * * @example Formatting an address with a name * ```ts * import { type Address, formatAddress } from "@upyo/core/address"; * const address: Address = { name: "John Doe", address: "john@example.com" }; * console.log(formatAddress(address)); // "John Doe <john@example.com>" * ``` * * @example Formatting an address without a name * ```ts * import { type Address, formatAddress } from "@upyo/core/address"; * const address: Address = { address: "jane@examle.com" }; * console.log(formatAddress(address)); // "jane@example.com" * ``` * * @param address The address object to format. * @return A string representation of the address. */ export function formatAddress(address: Address): string { return address.name == null ? address.address : `${address.name} <${address.address}>`; } /** * Parses a string representation of an email address into an {@link Address} * object. This function is an inverse of the {@link formatAddress} function. * * @example Parsing an address with a name * ```ts * import { parseAddress } from "@upyo/core/address"; * const address = parseAddress("John Doe <john@example.com>"); * console.log(address); // { name: "John Doe", address: "john@example.com" } * ``` * * @example Parsing an address without a name * ```ts * import { parseAddress } from "@upyo/core/address"; * const address = parseAddress("jane@example.com"); * console.log(address); // { address: "jane@example.com" } * ``` * * @example Trying to parse an invalid address * ```ts * import { parseAddress } from "@upyo/core/address"; * const address = parseAddress("invalid-email"); * console.log(address); // undefined * ``` * * @param address The string representation of the address to parse. * @returns An {@link Address} object if the parsing is successful, * or `undefined` if the input is invalid. */ export function parseAddress(address: string): Address | undefined { if (!address || typeof address !== "string") { return undefined; } const trimmed = address.trim(); if (!trimmed) { return undefined; } // Check for name and angle bracket format: "Name <email@domain.com>" const nameAngleBracketMatch = trimmed.match(/^(.+?)\s*<(.+?)>$/); if (nameAngleBracketMatch) { const name = nameAngleBracketMatch[1].trim(); const email = nameAngleBracketMatch[2].trim(); if (!isValidEmail(email)) { return undefined; } // Remove quotes from name if present const cleanName = name.replace(/^"(.+)"$/, "$1"); return { name: cleanName, address: email as EmailAddress }; } // Check for angle bracket format without name: "<email@domain.com>" const angleBracketMatch = trimmed.match(/^<(.+?)>$/); if (angleBracketMatch) { const email = angleBracketMatch[1].trim(); if (!isValidEmail(email)) { return undefined; } return { address: email as EmailAddress }; } // Check for plain email format: "email@domain.com" if (isValidEmail(trimmed)) { return { address: trimmed as EmailAddress }; } return undefined; } function isValidEmail(email: string): boolean { if (!email || typeof email !== "string") { return false; } // Find the @ symbol that separates local and domain parts // If the local part is quoted, we need to find the @ after the closing quote let atIndex = -1; let inQuotes = false; for (let i = 0; i < email.length; i++) { const char = email[i]; if (char === '"' && (i === 0 || email[i - 1] !== "\\")) { inQuotes = !inQuotes; } else if (char === "@" && !inQuotes) { if (atIndex === -1) { atIndex = i; } else { // Multiple @ symbols outside quotes return false; } } } if (atIndex === -1) { return false; } const localPart = email.substring(0, atIndex); const domainPart = email.substring(atIndex + 1); return isValidLocalPart(localPart) && isValidDomainPart(domainPart); } function isValidLocalPart(localPart: string): boolean { if (!localPart || localPart.length === 0 || localPart.length > 64) { return false; } // Check for quoted string format if (localPart.startsWith('"') && localPart.endsWith('"')) { const quotedContent = localPart.slice(1, -1); // Quoted strings can contain most characters, including @ symbol // Simple validation: no unescaped quotes or newlines let isValid = true; for (let i = 0; i < quotedContent.length; i++) { const char = quotedContent[i]; if (char === '"' || char === "\r" || char === "\n") { // Check if it's escaped if (i === 0 || quotedContent[i - 1] !== "\\") { isValid = false; break; } } } return isValid; } // Check for dot-atom format (RFC 5322) // No leading/trailing dots, no consecutive dots if ( localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes("..") ) { return false; } // Valid characters for dot-atom: alphanumeric, and some special chars const validLocalPartRegex = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*$/; return validLocalPartRegex.test(localPart); } function isValidDomainPart(domainPart: string): boolean { if (!domainPart || domainPart.length === 0 || domainPart.length > 253) { return false; } // Check for domain literal format [IP] if (domainPart.startsWith("[") && domainPart.endsWith("]")) { const literal = domainPart.slice(1, -1); // Use URL.canParse() for IP validation try { return URL.canParse(`http://${literal}/`); } catch { return false; } } // For regular domain format, use URL.canParse() for validation try { // URL.canParse() expects a valid URL, so we prepend http:// return URL.canParse(`http://${domainPart}/`); } catch { return false; } } /** * Type guard function that checks if a given value is a valid email address. * * @example * ```ts * import { isEmailAddress } from "@upyo/core/address"; * * const userInput = "user@example.com"; * if (isEmailAddress(userInput)) { * // TypeScript now knows userInput is EmailAddress type * console.log(userInput); // Type: `${string}@${string}` * } * ``` * * @param email The value to check * @returns `true` if the value is a valid email address, `false` otherwise */ export function isEmailAddress(email: unknown): email is EmailAddress { return typeof email === "string" && isValidEmail(email); }