mailer

send emails with cloudflare and mailchannels

mailer.md

resources

mailer.ts

import { z } from 'zod';
import { Logger } from '@lib/logger';

const logger = new Logger('mailer');

export class Mailer {
	static async send(email: Email) {
		const payload: MailerEmail = convert.email(email);

		// send email through MailChannels
		const resp = await fetch(
			new Request('https://api.mailchannels.net/tx/v1/send', {
				method: 'POST',
				headers: {
					'content-type': 'application/json',
				},
				body: JSON.stringify(payload),
			})
		);

		// check if email was sent successfully
		if (resp.status > 299 || resp.status < 200) {
			throw new Error(`Error sending email: ${resp.status} ${resp.statusText}`);
		}

		// log email sent
		const fromAddress = typeof email.from === 'string' ? email.from : email.from.email;
		const extractToAddress = (contact: Contact) => typeof contact === 'string' ? contact : contact.email;
		const toAddress = Array.isArray(email.to) ? email.to.map(extractToAddress).join(',') : extractToAddress(email.to);

		logger.info(`email sent from ${fromAddress} to ${toAddress}`);
	}
}

const ContactSchema = z.union([
	z.string(),
	z.object({
		email: z.string(),
		name: z.union([z.string(), z.undefined()]),
	}),
]);

const EmailSchema = z.object({
	to: z.union([ContactSchema, z.array(ContactSchema)]),
	replyTo: z.union([ContactSchema, z.array(ContactSchema)]).optional(),
	cc: z.union([ContactSchema, z.array(ContactSchema)]).optional(),
	bcc: z.union([ContactSchema, z.array(ContactSchema)]).optional(),
	from: ContactSchema,
	subject: z.string(),
	text: z.union([z.string(), z.undefined()]),
	html: z.union([z.string(), z.undefined()]),
});

type Contact = z.infer<typeof ContactSchema>;
export type Email = z.infer<typeof EmailSchema>;

/**
 * These Mailer types and interfaces correspond to the MailChannels API.
 */
type MailerPersonalization = { to: MailerContact[] };
type MailerContact = { email: string; name: string | undefined };
type MailerContent = { type: string; value: string };

interface MailerEmail {
	personalizations: MailerPersonalization[];
	from: MailerContact;
	reply_to: MailerContact | undefined;
	cc: MailerContact[] | undefined;
	bcc: MailerContact[] | undefined;
	subject: string;
	content: MailerContent[];
}

const convert = {
	/**
	 * Converts a Contact(s) or Contact[] to a MailerContact[]
	 */
	contacts(contacts: Contact | Contact[]): MailerContact[] {
		if (!contacts) {
			return [];
		}

		const contactArray: Contact[] = Array.isArray(contacts) ? contacts : [contacts];
		const convertedContacts: MailerContact[] = contactArray.map((contact: Contact): MailerContact => {
			if (typeof contact === 'string') {
				return { email: contact, name: undefined };
			}

			return { email: contact.email, name: contact.name };
		});

		return convertedContacts;
	},

	/**
	 * Converts an Email to a MailerEmail
	 */
	email(email: Email): MailerEmail {
		const personalizations: MailerPersonalization[] = [];

		// Convert 'to' field
		const toContacts: MailerContact[] = convert.contacts(email.to);
		personalizations.push({ to: toContacts });

		let replyTo: MailerContact | undefined = undefined;
		let bccContacts: MailerContact[] | undefined = undefined;
		let ccContacts: MailerContact[] | undefined = undefined;

		// Convert 'replyTo' field
		if (email.replyTo) {
			const replyToContacts: MailerContact[] = convert.contacts(email.replyTo);
			replyTo = replyToContacts.length > 0 ? replyToContacts[0] : { email: '', name: undefined };
		}

		// Convert 'cc' field
		if (email.cc) {
			ccContacts = convert.contacts(email.cc);
		}

		// Convert 'bcc' field
		if (email.bcc) {
			bccContacts = convert.contacts(email.bcc);
		}

		const [from] = convert.contacts(email.from);

		// Convert 'subject' field
		const subject: string = email.subject;

		// Convert 'text' field
		const textContent: MailerContent[] = [];
		if (email.text) {
			textContent.push({ type: 'text/plain', value: email.text });
		}

		// Convert 'html' field
		const htmlContent: MailerContent[] = [];
		if (email.html) {
			htmlContent.push({ type: 'text/html', value: email.html });
		}

		const content: MailerContent[] = [...textContent, ...htmlContent];

		return {
			personalizations,
			from,
			cc: ccContacts,
			bcc: bccContacts,
			reply_to: replyTo,
			subject,
			content,
		};
	}
}