Very Simple Way to Send Email Using NodeMCU firmware

Maybe some of you have already thought about sending email from NodeMCU powered module (ESP8266 ESP-01, ESP-07, etc). Maybe some of you have successfully written a code to send email from this little board.
Now I want to show you a very simple way to implement email sending. I think there are a lot of way to do it, but instead of writing a code to send email directly using an smtp server I will use third party tool for it. The name of the service is MailGun.
I have read an article which described a method using Mailgun for sending email. When I installed this Ghost Blog Engine, I got a warning message which showed that email service had not been configured properly, and gave me a link. In this article you can read about ghost email configuration, and it has a part about Mailgun configuration. Now I'm using this solution in my Ghost instance. But it is actually not important. While I was configuring Mailgun, I found an example api call using curl to send email via Mailgun. Link.
The API example:

curl -s --user 'api:YOUR_API_KEY' \ \
    -F from='Excited User <mailgun@YOUR_DOMAIN_NAME>' \
    -F \
    -F subject='Hello' \
    -F text='Testing some Mailgun awesomness!'

Some days later I was thinking. This curl example is a very simple and NodeMCU firmware is able to send HTTP POST messages. OK, maybe at the first sight it is not clear, but this curl example is actually a POST message. To come to the NodeMCU firmware, you have to build your firmware with HTTP module.
To see what kind of messages are sent over the network after this curl example I created a tcpdump. It is important to send this request without SSL. Because with SSL connection you won't see anything in the dump. Example:

curl -s --user 'api:key-2bdec103ac5dea85b9378ab2541faecf' \ \
    -F from='' \
    -F \
    -F subject='Hello' \
    -F text='Testing some Mailgun awesomness!'

This request returns with HTTP 404, but we are interested in only the request, not the response. Command: sudo tcpdump -s0 host -w pcap.pcap
This will create a pcap.pcap file, which can be opened in Wireshark.
We need the POST message to follow:

As I have written above we need only the request:

POST /v3/ HTTP/1.1  
Authorization: Basic YXBpOmtleS0yYmRlYzEwM2FjNWRlYTg1YjkzNzhhYjI1NDFmYWVjZg==  
User-Agent: curl/7.38.0  
Accept: */*  
Content-Length: 507  
Expect: 100-continue  
Content-Type: multipart/form-data; boundary=------------------------40dcfa2d67b56270

HTTP/1.1 100 Continue

Content-Disposition: form-data; name="from"  
Content-Disposition: form-data; name="to"  
Content-Disposition: form-data; name="subject"

Content-Disposition: form-data; name="text"

Testing some Mailgun awesomness!  

Ok. The first thing to do is figure out what is the Authorization: Basic YXBpOmtleS0yYmRlYzEwM2FjNWRlYTg1YjkzNzhhYjI1NDFmYWVjZg== line. This very simple because HTTP Basic auth is base64 encoded so we can decrypt it by using tihs command:
echo -n "YXBpOmtleS0yYmRlYzEwM2FjNWRlYTg1YjkzNzhhYjI1NDFmYWVjZg==" | base64 -d
The result:

So the basic auth HTTP header contains your MailGun API key. This step is not necessary, I was only curious.
If you do not want to create tcpdump to find out your basic auth key, simply use base64 command:

echo -n "api:key-2bdec103ac5dea85b9378ab2541faecf" | base64  

After that we have the first line of the header:

Authorization: Basic YXBpOmtleS0yYmRlYzEwM2FjNWRlYTg1YjkzNzhhYjI1NDFmYWVjZg==  

Based on the tcpdump we can assemble the HTTP header. You can see that MailGun API is using multipart/form-data MIME format, thus we have to use uniq boundary. If you want to know more about boundary or form-data MIME format please read the rfc2388 documentation. I will use dynamic generated boundary to avoid occurrence of it in the other part of the message.
I will use table.insert and table.concat to concatenate strings, because this method consume less memory then the simple .. (two dots).
So insted of this:
variable="string"..var.."another string"
I will use:

table.insert(test_table, "string")  
table.insert(test_table, var)  
table.insert(test_table, "another string")  

OK. Maybe it is a bit longer but it's worth it. Please remember to nil the table after concat.

1. Generate Boundary
for i=1,15  
    table.insert(boundary_table, string.char(math.random(65, 90))) -- A-Z
    table.insert(boundary_table, string.char(math.random(48, 57))) -- 0-9
    table.insert(boundary_table, string.char(math.random(97, 122))) -- a-z

This will create something like this: B2bO4oH6gL2iZ7oK6jG2zA6vK3zO1wI9dG1gP0wV0tE3p
OK. I know this is a bit stupid way to generate random string, but working and just enough for us.

2. Assemble the Header Part
table.insert(header_table, 'Authorization: Basic YXBpOmtleS0yYmRlYzEwM2FjNWRlYTg1YjkzNzhhYjI1NDFmYWVjZg==\r\n')  
table.insert(header_table, 'Host:\r\n')  
table.insert(header_table, 'User-Agent: NodeMCU/testAg\r\n')  
table.insert(header_table, 'Content-Type: multipart/form-data; boundary='..boundary..'\r\n')  

As you can see I inserted only the minimal necessary lines to the header.
Now we have the header variable with these lines:

Authorization: Basic YXBpOmtleS0yYmRlYzEwM2FjNWRlYTg1YjkzNzhhYjI1NDFmYWVjZg==  
User-Agent: NodeMCU/testAg  
Content-Type: multipart/form-data; boundary=V1tS3eX4rA9sK6pV2nN2uD5zP7qP7uT5qV6lL0zI8pA4u  
3. Assemble The "Body" Part (POST message)
table.insert(data_table, '--'..boundary..'\r\n')  
table.insert(data_table, 'Content-Disposition: form-data; name="from"\r\n\r\n')  
table.insert(data_table, '\r\n')  
table.insert(data_table, '--'..boundary..'\r\n')  
table.insert(data_table, 'Content-Disposition: form-data; name="to"\r\n\r\n')  
table.insert(data_table, '\r\n')  
table.insert(data_table, '--'..boundary..'\r\n')  
table.insert(data_table, 'Content-Disposition: form-data; name="subject"\r\n\r\n')  
table.insert(data_table, 'Hello\r\n')  
table.insert(data_table, '--'..boundary..'\r\n')  
table.insert(data_table, 'Content-Disposition: form-data; name="text"\r\n\r\n')  
table.insert(data_table, 'Congratulations Vincze Janos, you just sent an email with Mailgun!  You are truly awesome!\r\n\r\n')  
table.insert(data_table, '--'..boundary..'--\r\n')  

data values contains the following:

Content-Disposition: form-data; name="from"  
Content-Disposition: form-data; name="to"  
Content-Disposition: form-data; name="subject"

Content-Disposition: form-data; name="text"

Congratulations Vincze Janos, you just sent an email with Mailgun!  You are truly awesome!


There are some very important things:

  • In the header part use your "raw" boundary value:
    Content-Type: multipart/form-data; boundary=V1tS3eX4rA9sK6pV2nN2uD5zP7qP7uT5qV6lL0zI8pA4u
  • In the body part
    • You have to place two - sign before each boundaries:
      Content-Disposition: form-data; name="from"
    • BUT In case of the last boundary you have to add two - to the beginning and to the end of the boundary.:
4. Send An E-Mail :)

Finally we can now send the email.'', header, data,  
   function(code, data)
    if (code < 0) then
      print("HTTP request failed")
      print(code, data)

This message should be returned HTTP/200 OK after

>'', header, post_data,
>> function(code, data)
>> if (code < 0) then
>> print("HTTP request failed")
>> else
>> print(code, data)
>> end
>> end)
> 200    {
  "id": "<>",
  "message": "Queued. Thank you."

And my message is delivered to my mailbox. :)

If you don't like this method, you can find many other ways to implement email sending, or you can write a code on your own. For example on github there is implementation which uses smtp communication with NodeMCU net Module.
Honestly I don't like sending email directly from the ESP9266 modules because it has very limited resources, but there are some cases when you can implement this code. For example if your code has little footprint. So If you write a rather "big" and complex code it is possible that not enough memory will be left to assemble the header and body part and call Rather than sending email directly from esp8266 I advise to use NodeRED.

Vincze Janos Istvan

Read more posts by this author.


Subscribe to Blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!