Gmail Unread Email Counter using an esp8266-01 + 128×32 OLED + a self-hosted MQTT Server.

Build an esp8266 device that displays total unread Gmail messages on an 128×32 OLED using an “esp-01”. Also, set up a self-hosted MQTT server to handle messaging.

In the following post, I will retrace my steps and document how I:

  • Created a python script that polls my Gmail data every 10 seconds looking for unread messages total and sends that data to the esp8266-01 via MQTT.
  • Setup an MQTT “broker” on a home linux server for routing messages from the python script from the previous step (and for future IoT projects).
  • Flashed an esp8266-01 with a sketch to receive MQTT messages and display them on the OLED.

Note: You can use a Raspberry Pi instead of the esp8266: https://pseudo-server.com/blog/gmail-total-unread-messages-display-powered-by-a-raspberry-pi-zero-oled/.

Things Needed for This Project:

(*info: most links in the list below link to Amazon items through my Amazon Affiliate account. It’s often less expensive to search on eBay if you don’t mind waiting for shipments from China, etc.) :

Here are most of the items laid out on my table:

Hardware Ingredients esp8266-01, 3v3 logic converter, 128x32 OLED display.

1) Run through the official Gmail API for python tutorial and set up the “quickstart.py” script to get authenticated via cli.

Step through this google tutorial entirely making sure you get the “quickstart.py” script to fully work as described. Note: In stepping through this google tut you will also generate both a “credentials.json” file and a “token.pickle” file – both are needed for authentication with the API:

https://developers.google.com/gmail/api/quickstart/python

2) Tweak the “quickstart.py ” you just made.

Following the previous tutorial, let’s add a function to grab emails based on special query args like “label:unread category:primary“. Later we’ll add an MQTT library & logic to forward the message to the esp8266-01.

Add a new function to “quickstart.py” taken from the official docs: https://developers.google.com/gmail/api/v1/reference/users/messages/list.

This is the new function we’re going to use:

def ListMessagesMatchingQuery(service, user_id, query=''):
  """List all Messages of the user's mailbox matching the query.

  Args:
    service: Authorized Gmail API service instance.
    user_id: User's email address. The special value "me"
    can be used to indicate the authenticated user.
    query: String used to filter messages returned.
    Eg.- 'from:[email protected]_domain.com' for Messages from a particular sender.

  Returns:
    List of Messages that match the criteria of the query. Note that the
    returned list contains Message IDs, you must use get with the
    appropriate ID to get the details of a Message.
  """
  try:
    response = service.users().messages().list(userId=user_id,
                                               q=query).execute()
    messages = []
    if 'messages' in response:
      messages.extend(response['messages'])

    while 'nextPageToken' in response:
      page_token = response['nextPageToken']
      response = service.users().messages().list(userId=user_id, q=query,
                                         pageToken=page_token).execute()
      messages.extend(response['messages'])

    return messages
  except:
    print('An error occurred:')

Grab the unread email count by calling the new function with some query arguments. We’ll add this toward the bottom:

email_count = len(ListMessagesMatchingQuery(service, "me", "label:unread category:primary"))

Here is the full code so far with the additions:

from __future__ import print_function
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import paho.mqtt.client as paho

# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

def ListMessagesMatchingQuery(service, user_id, query=''):
  """List all Messages of the user's mailbox matching the query.

  Args:
    service: Authorized Gmail API service instance.
    user_id: User's email address. The special value "me"
    can be used to indicate the authenticated user.
    query: String used to filter messages returned.
    Eg.- 'from:[email protected]_domain.com' for Messages from a particular sender.

  Returns:
    List of Messages that match the criteria of the query. Note that the
    returned list contains Message IDs, you must use get with the
    appropriate ID to get the details of a Message.
  """
  try:
    response = service.users().messages().list(userId=user_id,
                                               q=query).execute()
    messages = []
    if 'messages' in response:
      messages.extend(response['messages'])

    while 'nextPageToken' in response:
      page_token = response['nextPageToken']
      response = service.users().messages().list(userId=user_id, q=query,
                                         pageToken=page_token).execute()
      messages.extend(response['messages'])

    return messages
  except:
    print('An error occurred:')

def main():
    """Shows basic usage of the Gmail API.
    Lists the user's Gmail labels.
    """
    creds = None
    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)

    service = build('gmail', 'v1', credentials=creds)
    
    email_count = len(ListMessagesMatchingQuery(service, "me", "label:unread category:primary"))

    print("Email count:", email_count)     

if __name__ == '__main__':
    main()

Try it out:

% python quickstart.py

It should print the unread email total in your console:

It worked! 4 unread emails. Now let’s route this data to an MQTT broker to eventually display on the OLED display.

3) Setup an MQTT broker (server) & edit “quickstart.py” to push messages to it.

I followed this tutorial excluding the SSL encryption (skip SSL steps 2,3,4 & 6. It’s a little out of date but works – also it’s incredibly easy to do): https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-the-mosquitto-mqtt-messaging-broker-on-ubuntu-16-04

This will set you up with an MQTT broker on a server. I just set mine up on a new VM. Since I’m not currently using this outside my home network, I did not need to forward any ports on my router.

If you’ve successfully completed setting up your broker and have tested it as-per the above tutorial, let’s add logic to “quickstart.py” to message our new MQTT broker with the unread email total.

Install the “paho -mqtt ” MQTT python library:

% pip install paho-mqtt

With paho-mqtt installed, we’ll add the library to “quickstart.py”:

import paho.mqtt.client as paho

Next, we’ll add the following vars toward the top:

### BEGIN Editable Vars
GMAIL_QUERY_ARGS = "label:unread category:primary"
MQTT_BROKER = "localhost"
MQTT_PORT = 1883
MQTT_TOPIC = "test" # keep as-is this for now unless you know what you're doing
MQTT_CLIENT_NAME = "control1" # no need to alter this
MQTT_USER = "justin" 
MQTT_PASS = "password"
MQTT_MESSAGE_APPEND = " Emails" # Append " Emails" to the message. The response will look like "7 Emails"
### END Editable Vars

Be sure to edit the above variables to match your settings. You’ll want to update “localhost” to the IP of your MQTT server and also update your username/password.

Then, we’ll add this MQTT logic toward the bottom after the “email_count” variable:

   try:
        broker=MQTT_BROKER
        port=MQTT_PORT
        def on_publish(client,userdata,result):
            print("data published \n")
            pass
        client1= paho.Client(MQTT_CLIENT_NAME)
        client1.on_publish = on_publish
        client1.username_pw_set(MQTT_USER, password=MQTT_PASS)
        client1.connect(broker,port)
        ret= client1.publish(MQTT_TOPIC,str(email_count)+MQTT_MESSAGE_APPEND)
    except:
        print("something went wrong")

The full (final) “quickstart.py” code with all the additions we’re going to be making to it should look like this:

from __future__ import print_function
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import paho.mqtt.client as paho

### BEGIN Editable Vars
GMAIL_QUERY_ARGS = "label:unread category:primary"
MQTT_BROKER = "localhost"
MQTT_PORT = 1883
MQTT_TOPIC = "test" # keep as-is this for now unless you know what you're doing
MQTT_CLIENT_NAME = "control1" # no need to alter this
MQTT_USER = "justin" 
MQTT_PASS = "password"
MQTT_MESSAGE_APPEND = " Emails" # Append " Emails" to the message. The response will look like "7 Emails"
### END Editable Vars

# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

def ListMessagesMatchingQuery(service, user_id, query=''):
  """List all Messages of the user's mailbox matching the query.

  Args:
    service: Authorized Gmail API service instance.
    user_id: User's email address. The special value "me"
    can be used to indicate the authenticated user.
    query: String used to filter messages returned.
    Eg.- 'from:[email protected]_domain.com' for Messages from a particular sender.

  Returns:
    List of Messages that match the criteria of the query. Note that the
    returned list contains Message IDs, you must use get with the
    appropriate ID to get the details of a Message.
  """
  try:
    response = service.users().messages().list(userId=user_id,
                                               q=query).execute()
    messages = []
    if 'messages' in response:
      messages.extend(response['messages'])

    while 'nextPageToken' in response:
      page_token = response['nextPageToken']
      response = service.users().messages().list(userId=user_id, q=query,
                                         pageToken=page_token).execute()
      messages.extend(response['messages'])

    return messages
  except:
    print('An error occurred:')

def main():
    """Shows basic usage of the Gmail API.
    Lists the user's Gmail labels.
    """
    creds = None
    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)

    service = build('gmail', 'v1', credentials=creds)
    
    email_count = len(ListMessagesMatchingQuery(service, "me", GMAIL_QUERY_ARGS))

   print("Email count:", email_count)

    try:
        broker=MQTT_BROKER
        port=MQTT_PORT
        def on_publish(client,userdata,result):
            print("data published \n")
            pass
        client1= paho.Client(MQTT_CLIENT_NAME)
        client1.on_publish = on_publish
        client1.username_pw_set(MQTT_USER, password=MQTT_PASS)
        client1.connect(broker,port)
        ret= client1.publish(MQTT_TOPIC,str(email_count)+MQTT_MESSAGE_APPEND)
    except:
        print("something went wrong")

if __name__ == '__main__':
    main()

Here is a version you can use if you want the sum total unread emails of multiple gmail accounts: https://gist.github.com/pseudo-projects/1318eb4dde9dbfb61c540e0ea94f6337

With everything setup, your MQTT broker should now receive the unread email total data as something like “7 Emails” when “quickstart.py” is run:

% python quickstart.py

Open another console like you did in the digitalocean.com tutorial previously and run the MQTT “sub” command to see if the message comes through when you run “quickstart.py”:

% sudo mosquitto_sub -h localhost -t test -u "justin" -P "<password>"

You should get some output like this:

Running quickstart.py
Running “quickstart.py” while “mosquito_sub” listens on the MQTT server.
Running a console on the MQTT broker to view messages.
“mosquito_sub” on the MQTT server receiving the message.

Now that the MQTT broker is successfully receiving the up-to-date unread email data from Gmail, let’s move the “quickstart.py” and associated auth files (“credentials.json” and “token.pickle” files) to a place where we can let it run forever. I put mine on the same server as MQTT. (note: it is easier to get the Gmail stuff working on your local machine and later move it as we’re doing now – you’ll just have to remember to install all the python libs on the new machine). Just update the “broker” value in “quickstart.py” to “localhost” if you put it on the same server as the MQTT broker.

With your “quickstart.py” and associated files moved to the new destination and running properly, we’ll use the “watch” command and the “screen” utility to keep the script running forever.

% screen 
% watch -n 10 python ./quickstart.py

Now “quickstart.py” will run “forever” checking for unread emails every 10 seconds. Keep the console open now for debugging, but you can hit “C+a d” to detach from screen and keep the process running – that’s ultimately the idea. (screen docs) – also, feel free to rename “quickstart.py” to something better at this point.

Now “quickstart.py” should be messaging the MQTT broker every 10 seconds with the unread email total data.

Let’s create a crontab to restart the above command on reboot (replacing my paths/filename with yours):

% crontab -e
@reboot screen && watch -n 10 python /home/justin/projects/gmail-unread-message-counter/quickstart.py

4) Setup the esp8266-01 to receive MQTT messages and display them on the OLED display.

Most of the following sketch was adapted from this blog: https://www.itsalllost.com/esp-01-oled-ssd1306/

Here is the full esp8266 sketch:

//Credit: https://www.itsalllost.com/esp-01-oled-ssd1306/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
 
// not available on ESP-01, but needed in the code
#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);

//BEGIN Editable Vars
const char* ssid = "<wifi-ssid>"; // ssid
const char* password = "<wifi-password>";  // password
const char* mqtt_server = "192.168.1.125"; // mqqt server
const char* mqtt_user = "justin";      
const char* mqtt_password = "password";  
const char* inTopic = "test"; // topic esp will subscribe to 
const char* clientId = "esp-testing"; // id of the esp - not so important for this
const char* email_address = "youremai[email protected]";
const char* ip_address = "192.168.1.124";
//END Editable Vars

WiFiClient espClient;

long lastMsg = 0;
char msg[50];
int value = 0;

void callback(char* topic, byte* payload, unsigned int length) {
  display.clearDisplay();
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  String theText = "";
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
    theText += (char)payload[i];
  }
  Serial.println();
 
  String message = (char*)payload;

  Serial.println("message");
  Serial.println(theText);
 
  // if payload = X
  // if ((char)payload[0] == '1') {
  // on any payload
  if ((char)payload[0]) {
    // do stuff
    display.clearDisplay();
  //oled will wrap lines, single line stops at U, lol, at you get it...
  //display.print("abcdefghijklmnopqrstuvwxyz");
    display.clearDisplay();
    display.setTextColor(WHITE);
    display.setTextSize(1);
    display.setCursor(0,0);
    display.print(theText); 
    display.setCursor(0,10);
    display.print(email_address); 
    display.setCursor(0,20);
    display.print("IP: "); 
    display.print(ip_address);
    // if payload = 0, using this to clear the line
  } else if ((char)payload[0] == '0') {
    // do stuff
    display.clearDisplay();
    display.setTextColor(BLACK);
    display.setTextSize(1);
    display.setCursor(0,0); 
    display.print("....................."); 
  }
}

PubSubClient client(mqtt_server, 1883, callback, espClient);
 
void setup()
{
  // set gpio0 and gpio2 as sda, scl
  Wire.begin(0,2);
  // initialize with the I2C addr 0x3C (for the 128x32)(initializing the display)
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  Serial.begin(9600);
  display.clearDisplay();
    display.setTextColor(WHITE);
    display.setTextSize(1);
    display.setCursor(0,0); 
    display.print("Hello World!"); 
  setup_wifi();  
  
}
 
void setup_wifi() {
  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
 
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
 
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect(clientId, mqtt_user, mqtt_password)) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      // client.publish("outTopic", "hello world");
      // ... and resubscribe
      client.subscribe(inTopic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}
void loop()
{
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  display.display();
}

Edit the following values in the sketch to fit your settings:

//BEGIN Editable Vars
const char* ssid = "<wifi-ssid>"; // ssid
const char* password = "<wifi-password>";  // password
const char* mqtt_server = "192.168.1.125"; // mqqt server
const char* mqtt_user = "justin";      
const char* mqtt_password = "password";  
const char* inTopic = "test"; // topic esp will subscribe to 
const char* clientId = "esp-testing"; // id of the esp - not so important for this
const char* email_address = "[email protected]";
const char* ip_address = "192.168.1.124";
//END Editable Vars

It is using the Wire.h library for software I2C on GPIO pins 0, 2 (sda,scl respectively).

esp8266-01 pinout

Flash that sketch onto your esp8266-01 and connect the OLED display. If everything went smoothly, your esp8266 should be connected to Wifi, listening to your MQTT server for messages and displaying them on the OLED.

Gmail total unread emails displayed on the OLED via the esp8266-01 and self-hosted MQTT broker.

The esp8266-01 is a great device. There are plenty of tutorials out there showing you how to work with it using the Arduino IDE. If you made it this far and it is not working, you should test each component separately, the OLED, esp8266-01, MQTT server, “quickstart.py” etc. Make sure everything is working independently.

Add multiple wifi AP’s to the esp-01 sketch so you can flash it once and bring it with you.

If you want to take this outside your local network, you can use a feature from the esp8266 Arduino library to add multiple AP credentials so that the esp8266 will connect to the strongest AP signal it finds. This is useful in case you want to, say, bring it to work and put it on your desk without having to reflash it with the wifi creds of your office. Note: you also need to open the MQTT port on your router and forward it to your MQTT server, also update the “mqtt_server” value in the sketch.

Here is the full updated esp8266-01 sketch configured with 2 wifi AP’s(home/work wifi):

//Credit: https://www.itsalllost.com/esp-01-oled-ssd1306/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>   // Include the Wi-Fi-Multi library
#include <PubSubClient.h>

ESP8266WiFiMulti wifiMulti;     // Create an instance of the ESP8266WiFiMulti class, called 'wifiMulti'

// not available on ESP-01, but needed in the code
#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);

//BEGIN Editable Vars
const char* ssid_from_AP_1 = "<WIFI-SSID1>"; // ssid 1 (Home)
const char* your_password_for_AP_1 = "<WIFI-PASS1>";  // password 1
const char* ssid_from_AP_2 = "<WIFI-SSID2>"; // ssid 2 (Work)
const char* your_password_for_AP_2 = "<WIFI-PASS2>";  // password 2

const char* mqtt_server = "mydyns.dynu.com"; // mqqt server (after forwarding your port 1883)
const char* mqtt_user = "<MQTT-USERNAME>";      
const char* mqtt_password = "<MQTT-PASSWORD>";  
const char* inTopic = "test"; // topic esp will subscribe to 
const char* clientId = "esp-testing"; // id of the esp - not so important for this
const char* email_address = "[email protected]";
const char* ip_address = "192.168.1.124"; // Set to the local IP of your esp if you want (or display anything: "hello world")
//END Editable Vars

WiFiClient espClient;

long lastMsg = 0;
char msg[50];
int value = 0;

void callback(char* topic, byte* payload, unsigned int length) {
  display.clearDisplay();
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  String theText = "";
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
    theText += (char)payload[i];
  }
  Serial.println();
 
  String message = (char*)payload;

  Serial.println("message");
  Serial.println(theText);
 

  if ((char)payload[0]) {

    display.clearDisplay();
  //oled will wrap lines, single line stops at U, lol, at you get it...
  //display.print("abcdefghijklmnopqrstuvwxyz");
    display.clearDisplay();
    display.setTextColor(WHITE);
    display.setTextSize(1);
    display.setCursor(0,0);
    display.print(theText); 
    display.setCursor(0,10);
    display.print(email_address); 
    display.setCursor(0,20);
    display.print("IP: "); 
    display.print(ip_address);
  //if payload = 0, using this to clear the line
  } else if ((char)payload[0] == '0') {
    // do stuff
    display.clearDisplay();
    display.setTextColor(BLACK);
    display.setTextSize(1);
    display.setCursor(0,0); 
    display.print("....................."); 
  }
}

PubSubClient client(mqtt_server, 1883, callback, espClient);
 
void setup()
{
  
  // set gpio0 and gpio2 as sda, scl
  Wire.begin(0,2);
  // initialize with the I2C addr 0x3C (for the 128x32)(initializing the display)
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  Serial.begin(9600);
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0,0); 
  display.print("Hello World!"); 
  wifiMulti.addAP(ssid_from_AP_1, your_password_for_AP_1);   // add Wi-Fi networks you want to connect to
  wifiMulti.addAP(ssid_from_AP_2, your_password_for_AP_2);
  setup_wifi();  
  
}
 
void setup_wifi() {
  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting");
  int i = 0;
  while (wifiMulti.run() != WL_CONNECTED) { // Wait for the Wi-Fi to connect: scan for Wi-Fi networks, and connect to the strongest of the networks above
    delay(1000);
    Serial.print('.');
  }
  Serial.println('\n');
  Serial.print("Connected to ");
  Serial.println(WiFi.SSID()); // Tell us what network we're connected to
  Serial.print("IP address:\t");
  Serial.println(WiFi.localIP()); // Send the IP address of the ESP8266 to the computer
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect(clientId, mqtt_user, mqtt_password)) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      // client.publish("outTopic", "hello world");
      // ... and resubscribe
      client.subscribe(inTopic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}
void loop()
{
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  display.display();
}

Final Thoughts

This was a fun project to piece together. It may be too much effort if your IoT tinkering stops here. I am very excited about having a template-project now to build off of and display whatever data I want using 2 very tiny and very inexpensive modules.

Leave a Reply

Your email address will not be published. Required fields are marked *