UNPKG

mimetext

Version:

RFC-5322 compliant, fully typed and documented email message generator for javascript runtimes.

273 lines (247 loc) 8.49 kB
import type { EnvironmentContext } from "./MIMEMessage"; import { MIMETextError } from "./MIMETextError.js"; import { Mailbox } from "./Mailbox.js"; /* Headers are based on: https://www.rfc-editor.org/rfc/rfc4021#section-2.1 (Some are ignored as they can be added later or as a custom header.) */ export class MIMEMessageHeader { envctx: EnvironmentContext; fields: HeaderField[] = [ { name: "Date", generator: () => new Date().toUTCString().replace(/GMT|UTC/gi, "+0000"), }, { name: "From", required: true, validate: (v: unknown) => this.validateMailboxSingle(v), dump: (v: unknown) => this.dumpMailboxSingle(v), }, { name: "Sender", validate: (v: unknown) => this.validateMailboxSingle(v), dump: (v: unknown) => this.dumpMailboxSingle(v), }, { name: "Reply-To", validate: (v: unknown) => this.validateMailboxSingle(v), dump: (v: unknown) => this.dumpMailboxSingle(v), }, { name: "To", validate: (v: unknown) => this.validateMailboxMulti(v), dump: (v: unknown) => this.dumpMailboxMulti(v), }, { name: "Cc", validate: (v: unknown) => this.validateMailboxMulti(v), dump: (v: unknown) => this.dumpMailboxMulti(v), }, { name: "Bcc", validate: (v: unknown) => this.validateMailboxMulti(v), dump: (v: unknown) => this.dumpMailboxMulti(v), }, { name: "Message-ID", generator: () => { const randomstr = Math.random().toString(36).slice(2); const from = this.fields.filter( (obj) => obj.name === "From", )[0]!.value as Mailbox; const domain = from.getAddrDomain(); return "<" + randomstr + "@" + domain + ">"; }, }, { name: "Subject", required: true, dump: (v: unknown) => { return typeof v === "string" ? "=?utf-8?B?" + this.envctx.toBase64(v) + "?=" : ""; }, }, { name: "MIME-Version", generator: () => "1.0", }, ]; constructor(envctx: EnvironmentContext) { this.envctx = envctx; } dump(): string { let lines = ""; for (const field of this.fields) { if (field.disabled) continue; const isValueDefinedByUser = field.value !== undefined && field.value !== null; if (!isValueDefinedByUser && field.required) { throw new MIMETextError( "MIMETEXT_MISSING_HEADER", `The "${field.name}" header is required.`, ); } if (!isValueDefinedByUser && typeof field.generator !== "function") continue; if (!isValueDefinedByUser && typeof field.generator === "function") field.value = field.generator(); const strval = Object.hasOwn(field, "dump") && typeof field.dump === "function" ? field.dump(field.value) : typeof field.value === "string" ? field.value : ""; lines += `${field.name}: ${strval}${this.envctx.eol}`; } return lines.slice(0, -1 * this.envctx.eol.length); } toObject(): HeadersObject { return this.fields.reduce((memo: HeadersObject, item) => { memo[item.name] = item.value; return memo; }, {}); } get(name: string): string | Mailbox | Mailbox[] | undefined { const fieldMatcher = (obj: HeaderField): boolean => obj.name.toLowerCase() === name.toLowerCase(); const ind = this.fields.findIndex(fieldMatcher); return ind !== -1 ? this.fields[ind]!.value : undefined; } set(name: string, value: string | Mailbox | Mailbox[]): HeaderField { const fieldMatcher = (obj: HeaderField): boolean => obj.name.toLowerCase() === name.toLowerCase(); const isCustomHeader = !this.fields.some(fieldMatcher); if (!isCustomHeader) { const ind = this.fields.findIndex(fieldMatcher); const field = this.fields[ind]!; if (field.validate && !field.validate(value)) { throw new MIMETextError( "MIMETEXT_INVALID_HEADER_VALUE", `The value for the header "${name}" is invalid.`, ); } this.fields[ind]!.value = value; return this.fields[ind]!; } return this.setCustom({ name: name, value: value, custom: true, dump: (v: unknown) => (typeof v === "string" ? v : ""), }); } setCustom(obj: HeaderField): HeaderField { if (this.isHeaderField(obj)) { if (typeof obj.value !== "string") { throw new MIMETextError( "MIMETEXT_INVALID_HEADER_FIELD", "Custom header must have a value.", ); } this.fields.push(obj); return obj; } throw new MIMETextError( "MIMETEXT_INVALID_HEADER_FIELD", "Invalid input for custom header. It must be in type of HeaderField.", ); } validateMailboxSingle(v: unknown): v is Mailbox { return v instanceof Mailbox; } validateMailboxMulti(v: unknown): boolean { return v instanceof Mailbox || this.isArrayOfMailboxes(v); } dumpMailboxMulti(v: unknown): string { const dump = (item: Mailbox): string => item.name.length === 0 ? item.dump() : `=?utf-8?B?${this.envctx.toBase64(item.name)}?= <${item.addr}>`; return this.isArrayOfMailboxes(v) ? v.map(dump).join(`,${this.envctx.eol} `) : v instanceof Mailbox ? dump(v) : ""; } dumpMailboxSingle(v: unknown): string { const dump = (item: Mailbox): string => item.name.length === 0 ? item.dump() : `=?utf-8?B?${this.envctx.toBase64(item.name)}?= <${item.addr}>`; return v instanceof Mailbox ? dump(v) : ""; } isHeaderField(v: unknown): v is HeaderField { const validProps = [ "name", "value", "dump", "required", "disabled", "generator", "custom", ]; if (this.isObject(v)) { const h = v as HeaderField; if ( Object.hasOwn(h, "name") && typeof h.name === "string" && h.name.length > 0 ) { if ( !Object.keys(h).some((prop) => !validProps.includes(prop)) ) { return true; } } } return false; } isObject(v: unknown): v is object { return !!v && v.constructor === Object; } isArrayOfMailboxes(v: unknown): v is Mailbox[] { return ( this.isArray(v) && v.every((item: unknown) => item instanceof Mailbox) ); } isArray(v: unknown): v is never[] { return !!v && v.constructor === Array; } } export class MIMEMessageContentHeader extends MIMEMessageHeader { override fields = [ { name: "Content-ID", }, { name: "Content-Type", }, { name: "Content-Transfer-Encoding", }, { name: "Content-Disposition", }, ]; constructor(envctx: EnvironmentContext) { super(envctx); } } export type HeadersObject = Record< string, string | Mailbox | Mailbox[] | undefined >; export interface HeaderField { name: string; dump?: (v: string | Mailbox | Mailbox[] | undefined) => string; value?: string | Mailbox | Mailbox[] | undefined; validate?: (v: unknown) => boolean; required?: boolean; disabled?: boolean; generator?: () => string; custom?: boolean; }