UNPKG

@upyo/core

Version:

Simple email sending library for Node.js, Deno, Bun, and edge functions

155 lines (153 loc) 5.18 kB
//#region src/address.ts /** * 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. */ function formatAddress(address) { 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. */ function parseAddress(address) { if (!address || typeof address !== "string") return void 0; const trimmed = address.trim(); if (!trimmed) return void 0; const nameAngleBracketMatch = trimmed.match(/^(.+?)\s*<(.+?)>$/); if (nameAngleBracketMatch) { const name = nameAngleBracketMatch[1].trim(); const email = nameAngleBracketMatch[2].trim(); if (!isValidEmail(email)) return void 0; const cleanName = name.replace(/^"(.+)"$/, "$1"); return { name: cleanName, address: email }; } const angleBracketMatch = trimmed.match(/^<(.+?)>$/); if (angleBracketMatch) { const email = angleBracketMatch[1].trim(); if (!isValidEmail(email)) return void 0; return { address: email }; } if (isValidEmail(trimmed)) return { address: trimmed }; return void 0; } function isValidEmail(email) { if (!email || typeof email !== "string") return false; 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 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) { if (!localPart || localPart.length === 0 || localPart.length > 64) return false; if (localPart.startsWith("\"") && localPart.endsWith("\"")) { const quotedContent = localPart.slice(1, -1); let isValid = true; for (let i = 0; i < quotedContent.length; i++) { const char = quotedContent[i]; if (char === "\"" || char === "\r" || char === "\n") { if (i === 0 || quotedContent[i - 1] !== "\\") { isValid = false; break; } } } return isValid; } if (localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes("..")) return false; const validLocalPartRegex = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*$/; return validLocalPartRegex.test(localPart); } function isValidDomainPart(domainPart) { if (!domainPart || domainPart.length === 0 || domainPart.length > 253) return false; if (domainPart.startsWith("[") && domainPart.endsWith("]")) { const literal = domainPart.slice(1, -1); try { return URL.canParse(`http://${literal}/`); } catch { return false; } } try { 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 */ function isEmailAddress(email) { return typeof email === "string" && isValidEmail(email); } //#endregion exports.formatAddress = formatAddress; exports.isEmailAddress = isEmailAddress; exports.parseAddress = parseAddress;