@upyo/core
Version:
Simple email sending library for Node.js, Deno, Bun, and edge functions
155 lines (153 loc) • 5.18 kB
JavaScript
//#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;