MailChannels End of Life notice - migrate now!

MailChannels have recently announced that they will be discontinuing their free API service with Cloudflare Workers as of June 30th. You can find details here:

They also stated in the Cloudflare Discord server that:

Starting next week, you may notice a small fraction (1%) of your connections are rejected with an error message containing a link to the same announcement. Any messages that are accepted by the service will be delivered as usual and we will continue to provide support as usual until the final day of service.

So I would recommend folks migrate now!

Alternatives

I can confidently recommend both Postmark and SendGrid, and have heard good things about Resend.

The Cloudflare developer docs has great tutorials for sending with both Postmark and Resend:

11 Likes

I hate that I have to deal will a third party. Cloudflare should provide an alternative.

This is why people end up on AWS; they provide all that you need.

1 Like

Wow this was quite a stab in the back. I literally learned about it today when I looked at the logs and saw all those errors. At least sending a warning email in advance would be nice

2 Likes

I will leave here some prices so people can compare the services

Price per 5000 mail/month

resend: 20$/month
postmarkapp: 15$/month
sendgrid: 20$/month
aws: ~0.5$ to $1/month (outbound + attachments)

If you end up picking aws and you are on cloudflare pages I suggest to use aws4fetch as the official ses client does not work

here’s an example

import { AwsClient } from "aws4fetch";
import { adminEmail } from "site";

const generateEmailBody = (data: Record<string, any>) => {
	let htmlBody = "<h1>New Form Submission</h1><ul>";
	let textBody = "New Form Submission\n\n";

	for (const key in data) {
		const value = data[key];
		htmlBody += `<li><strong>${key}:</strong> ${value}</li>`;
		textBody += `${key}: ${value}\n`;
	}
	htmlBody += "</ul>";
	return { htmlBody, textBody };
};

const createSendEmailParams = (toAddress: string, fromAddress: string, inputData: Record<string, any>) => {
	const { htmlBody, textBody } = generateEmailBody(inputData);
	const params = new URLSearchParams();

	params.append("Action", "SendEmail");
	params.append("Version", "2010-12-01");
	params.append("Source", fromAddress);
	params.append("Destination.ToAddresses.member.1", toAddress);

	// Subject
	const subject = `xxx.com - new form submission from ${inputData?.email ?? "Unknown"}`;
	params.append("Message.Subject.Data", subject);
	params.append("Message.Subject.Charset", "UTF-8");

	// Text Body
	params.append("Message.Body.Text.Data", textBody);
	params.append("Message.Body.Text.Charset", "UTF-8");

	// HTML Body
	params.append("Message.Body.Html.Data", htmlBody);
	params.append("Message.Body.Html.Charset", "UTF-8");

	// ReplyToAddresses
	if (inputData?.email) {
		params.append("ReplyToAddresses.member.1", inputData.email);
	}

	return params.toString();
};


const aws = new AwsClient({
	accessKeyId: ctx.locals?.runtime?.env?.AWS_ACCESS_KEY_ID!, // replace with your environment variable
	secretAccessKey: ctx.locals?.runtime?.env?.AWS_SECRET_ACCESS_KEY!,  // replace with your environment variable
	region: ctx.locals?.runtime?.env?.AWS_REGION || "eu-central-1", // Replace with your AWS region
	service: "ses",
});

const endpoint = `https://email.${aws.region}.amazonaws.com/`;

const requestBody = createSendEmailParams(adminEmail, "[email protected]", input);

try {
	const response = await aws.fetch(endpoint, {
		method: "POST",
		headers: {
			"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
		},
		body: requestBody,
	});

	if (!response.ok) {
		const errorText = await response.text();
		throw new Error(`AWS SES SendEmail failed: ${errorText}`);
	}
} catch (caught) {
	if (caught instanceof Error && caught.name === "MessageRejected") {
		return caught;
	}
	throw caught;
}

Exactly, where was the notice to customers regarding this??!?!

How many emails have I missed due to this I wonder? I’m now adding a weekly check to check the email solution actually works.

This is not good!