Send email from Workers using MailChannels, for free

If you’re looking for a way to send email from Workers, that now exists, with no login, no domain verification, and no payment. MailChannels now provides an API endpoint that lets you send email to anyone, for free.

Our KB article describes how to get started here: Sending Email from Cloudflare Workers using MailChannels Send API

Snippet of Workers code follows to show how it works:

addEventListener("fetch", event => {
    event.respondWith(handleRequest(event.request))
})
 
async function handleRequest(request) {
    let content = "";
    for( var i of request.headers.entries() ) {
        content += i[0] + ": " + i[1] + "\n";
    }
    let send_request = new Request("https://api.mailchannels.net/tx/v1/send", {
        "method": "POST",
        "headers": {
            "content-type": "application/json",
        },
        "body": JSON.stringify({
            "personalizations": [
                { "to": [ {"email": "[email protected]",
                        "name": "Test Recipient"}]}
            ],
            "from": {
                "email": "[email protected]",
                "name": "Test Sender",
            },
            "subject": "Test Subject",
            "content": [{
                "type": "text/plain",
                "value": "Test message content\n\n" + content,
            }],
        }),
    });
 
    let respContent = "";
    // only send the mail on "POST", to avoid spiders, etc.
    if( request.method == "POST" ) {
        const resp = await fetch(send_request);
        const respText = await resp.text();
 
        respContent = resp.status + " " + resp.statusText + "\n\n" + respText;
 
    }
 
    let htmlContent = "<html><head></head><body><pre>" +
        "</pre><p>Click to send message: <form method="post"><input type="submit" value="Send"/></form></p>" +
        "<pre>" + respContent + "</pre>" +
        "</body></html>";
    return new Response(htmlContent, {
        headers: { "content-type": "text/html" },
    })
}
12 Likes

I modified the script a bit to use a simple form instead of hard coding it. Can you tell what I am doing wrong … I am keep getting 400 error…although it worked once but after that, I am repeatedly getting 400.
and “Content value must be at least one character in length.

addEventListener("fetch", event => {
    event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
    let content = "just drop if it fails...okay ?";

        let respContent = "";
        let fls = ""; 
    for( var i of request.headers.entries() ) {
        content += i[0] + ": " + i[1] + "\n";
    }

    let send_request = new Request("https://api.mailchannels.net/tx/v1/send", {
        "method": "POST",
        "headers": {
            "content-type": "application/json",
        },
        "body": JSON.stringify({
            "personalizations": [
                { "to": [ {"email": fls.email,
                        "name": fls.name}]}
            ],
            "from": {
                "email": "[email protected]",
                "name": "Amara Silva",
            },
            "subject": fls.subject,
            "content": [{
                "type": "text/plain",
                "value": fls.message,
            }],
        }),
    });

        // only send the mail on "POST", to avoid spiders, etc.
    if( request.method == "POST" ) {
    
    const formData = await request.formData();
    const body = {};
        for (const entry of formData.entries()) {
                      body[entry[0]] = entry[1];
              }
        fls = JSON.parse(JSON.stringify(body));

        const resp = await fetch(send_request);
        const respText = await resp.text();
 
        respContent = resp.status + " " + resp.statusText + "\n\n" + respText + fls.name; 
    }
 
    let htmlContent = "<html><head></head><body><pre>" +
        '</pre><p>Click to send message: <form method="post">Name: <input type="text" name="name"/><br>Email: <input type="text" name="email"/><br>Sub: <input type="text" name="subject"/><br>Msg: <input type="text" name="message"/><br><input type="submit" value="Send"/></form></p>' +
        "<pre>" + respContent + "</pre>" +
        "</body></html>";
    return new Response(htmlContent, {
        headers: { "content-type": "text/html" },
    })
}
1 Like

I think the problem is that when let send_request = new Request(...) is called, fls is still "". It gets reassigned to contain the form contents later, but at that point, the request has already been constructed.

1 Like

Thanks…I’ve fixed it now. Just called the new Request(...) later after the body has assigned all the form variables. Thanks.

2 Likes

Great to hear that you got it working, Suleman. Don’t hesitate to file a support ticket by emailing [email protected] if you have any issues that aren’t getting resolved here in the forums.

Thanks it’s okay. I was just testing it. I liked the fact that it cannot leveraged for spam and bulk mailing…that’s what I wanted to test it for.

i made something about this see comment instead · GitHub

What’s the limitation of this service?
On the free workers plan, we can make 100,000 requests per day. Does this mean we can send 100,000 emails using workers every day on the free plan?

What types of content are we allowed to send using workers?

  • Transactional emails?
  • Marketing / promotional emails?
  • Can we build a marketing platform on top of workers and have users of our app send marketing / promotional / newsletters using this?
1 Like

We don’t place any restrictions or arbitrary limits on what you can send. We do perform extensive spam and phishing filtering and have a state of the art system to ensure that bad email practices will be limited or blocked.

In the near future, we will offer paid accounts that provide access to metrics, logs, and other features that will help customers build larger services using the platform. If this is you, please get in touch with us and we will get something going for you.

The best way to get in touch for commercial questions is via [email protected]. For technical matters, please send to [email protected].

2 Likes

Thanks! I guess if the CEO says it’s ok, then…it’s better than a note from your mom. :+1:

1 Like

Agreed!

So basically, does this allow other developers using Workers to impersonate me and send emails from my domains (if I add SPF records)?

Any way to prevent third parties from sending emails on my behalf?

The sad truth is that anyone can impersonate your domain right now from a whole variety of services on the Internet. To protect yourself, we highly recommend setting up and properly using DMARC so that receivers know that email from your domain has to be signed and that it must come from only authorized sources.

The Messaging, Malware, Mobile Anti-Abuse Working Group (M3AAWG) recently published some guidelines on how to best protect your domain online. It’s a free download and is packed with information that will be useful to you: https://www.m3aawg.org/sites/default/files/m3aawgbrandprotectionkit_domainmanagement.pdf

3 Likes

So… how would one use DKIM signatures on mail sent via this API?

1 Like

There is a DKIM library available via npm: dkim-signature - npm

We haven’t tested it in Workers, but presumably you could sign messages using this library. I’ve promoted this topic with our team to see if they can find a better way while we work on adding it natively to our Send API.

“Presumably”?

So, if I send mail the way you have set things up here, it won’t have DKIM signatures, so I cannot have a DMARC policy that requires them. I must add your entry to my SPF record (of course). Thus, anyone can now send email from Workers using my domain, and it will be 100% compliant with my DMARC policy, and there is nothing in place to restrict this in any way.

Unless I create my own DKIM signatures in code in a way you haven’t ever even tested.

Is that about right?

1 Like

That is correct and it highlights a limitation of DMARC, which is that you cannot specify whether you want DMARC to enforce only DKIM or only SPF alignment. You have to have it all.

Rest assured we will provide DKIM signing natively. It’s just not there yet. I will try to get someone on my team to test the npm module out this week within Workers and, assuming it works, I’ll make a post here.

If you beat us to it, I will send you a t-shirt!

1 Like

Yeah…. about that…. Welcome to the internet. Dude seems like he is trying to build an interesting service, would be a shame if SMTP was inherently insecure by design. I mean I guess don’t use his service? Or presumably you could build your own.

4 Likes

The service seems to be great, but the issue is about security.

And you really missed the point here.

It must be wonderful to work with you taking constructive criticism personally!

Yep and the guy posted a possible approach and indicated he was having his dev team look into the issue.

Did I? Please feel free to explain it to me in detail. I look forward to your insights.

Presumably.

1 Like