mailer
send emails with cloudflare and mailchannels
mailer.md
resources
- MailChannels documentation to set up your domains
- Inspired by Sh4yy/cloudflare-email
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,
};
}
}