Tento projekt popisuje, jak si pomocí ESP8266 naprogramovat vlastní aplikaci, která bude pravidelně kontrolovat příchozí emaily a každý nový email zobrazí na display. ESP8266 jsem využil na devboardu zvaném NodeMCU, pokud víte jak, tak samozřejmě můžete využít i libovolné jiné ESP8266, jediná potřebná podmínka ale je, že musí mít pro aplikaci dostatečný počet portů (což třeba nejmenší ESP8266-01 nesplňuje). Pro zprovoznění postupujte podle https://navody.arduino-shop.cz/navody-k-produktum/esp8266-vyvojova-deska-wemos-d1.html a zvolte správnou desku (NodeMCU 1.0)

Projekt využívá vlastní integrovanou „knihovnu“ na obsluhu komunikace s emailovým serverem. Dále pak používá knihovny na práci s displayem a knihovnu pro base64. Knihovny jsem musel upravit. Knihovna k display neobsahovala podporu pro ESP8266, takže je použit její fork a knihovna pro base64 zase názvově kolidovala s integrovanou knihovnou pro ESP8266, která však neobsahuje všechny funkce, proto je dodaná knihovna přejmenovaná z base64 ba _base64 (přidáno podtržítko). Upravené knihovny jsou součástí balíku, na který najdete odkaz na konci článku.

Schéma zapojení

Projekt je potřeba připojit následujícím způsobem.

Propojení ESP s Nokia 5110 display

NodeMCU (ESP8266)

Nokia 5110 Display

D2 (GPIO 04)

RST

D1 (GPIO 05)

CE

D6 (GPIO 12)

DC

D7 (GPIO 13)

DIN

D5 (GPIO 14)

CLK

VCC (3.3V)

VCC

GND

LIGHT

GND

GND

Propojení LED

LED připojte k pinu D0 (GPIO 16) a druhým vývodem na GND. Pokud máte svou LED rádi, tak mezi ni a zem připojte ještě vhodný rezistor (např. 330Ω).

Propojení tlačítka

Jeden vývod tlačítka připojte na zem, druhý na D4 (GPIO 2). Na D4 (GPIO 2) ještě připojte z VCC pull-up rezistor nejlépe 4k7.

Celé zapojení může vypadat nějak takto:

Notifikace příchozího emailu ESP8266 schéma

Komunikace s emailovým serverem

Základem aplikace je komunikace s emailovým serverem. Emaily pracují s dvěma servery. Jeden pro odesílání pošty a jeden pro příjem pošty. Server pro odesílání se jmenuje SMTP a server pro přijímaní se jmenuje POP3 nebo modernější IMAP. My v této aplikaci potřebuje emaily přijímat, takže budu implementovat komunikaci pomocí protokolu POP3.

POP3

POP3 je jednoduchý protokol, vyvinutý v roce 1994 jako nástupce protokolů POP2 a POP1. Jen pro představu, protokol POP1 byl vyvinut v roce 1984. Popis tohoto protokolu je v RFC, konkrétně se jedná o RFC 1725. Je to jednoduchý protokol postavený nad TCP. Využívá TCP portu 110 a funguje na principu že klient (zde to bude naše ESP8266) odesílá příkazy a server na ty příkazy odesílá odpověď. Komunikace je čistě textová. Ve srovnání s jinými protokoly jsou příkazy velmi jednoduché. V RFC je najdete pospány všechny, nás budou zajímat příkazy user, pass a retr.

Autorizace

Na server se musíme přihlásit. Dělá se to tak, že se pošle příkaz user následovaný mezerou, dále pak emailovou adresou jejíž emaily chceme stáhnout a enterem. Enter je windowsový, tzn. složen ze dvou znaků CR LF (resp. rn). Server odpoví OK a jako další příkaz očekává příkaz pass kde obdobným způsobem odešleme heslo. Za zmínku stojí, že se jedná o protokol, který ve své standardní variantě neobsahoval žádné šifrování a tudíž v tomto bodě budeme odesílat heslo v čitelné podobě přes internet, což může být nebezpečné. Server odpoví číslem ukazující dostupný počet emailů.

Stažení emailu

Stažení probíhá pomocí příkazu retr, kterému jako parametr předáme pořadové čísle emailu, který chceme stáhnout. My budeme stahovat poslední email, takže předáme číslo, které vrátí server po autorizaci uživatele. Na tento příkaz odpoví server emailem.

Formát emailu

Každý email má své hlavičky a obsah. V obsahu mohou být zakomponovány přílohy nebo může email obsahovat i více obsahů (textovou a HTML variantu). Hlavičky obsahují různá metadata emailu, mohou být různé, nemusí být všechny a každý prvek v komunikaci (např. SPAM filtr) si může přidat svoje vlastní. Nás budou zajímat hlavičky From, což je odesílatel a Subject, což je předmět. Obsah emailu nás zajímat nebude. Nebude nás zajímat z několika důvodů. Prvním důvodem je, že formát obsahu je dost složitý a druhý důvod je, že se může stát a velmi rychle by se to stalo, že aplikace přestane fungovat při přijetí velkého emailu, protože se zkrátka a dobře velký email (například s tučnou přílohou) nevleze do (relativně malé) paměti na ESP8266. Fakticky tak z emailu přečteme jenom hlavičky, což ale stačí.

Formát hlaviček

Formát hlaviček je obdobný jako u protokolu http. Odesílá se název hlavičky, dvojtečka a hodnota. Končí se enterem. Podstatné ale je že obsah hodnoty může být kódován. Formát hlavičky je definován v RFC 2047, kde je napsáno, jakým způsobem může být (ale nemusí) hodnota kódována. Kódovaná hodnota začíná sekvencí =? Následovanou znakovým kódováním (nejčastěji UTF-8), dalším otazníkem, druhem kódování což může být b nebo g, otazníkem, kódovanou hodnotou podle g nebo b a koncovým ?=.

Kódování b

B značí base64 (proto je v projektu potřeba knihovna pro base64) a hodnota je tedy zakódována tímto algoritmem. Zájemcům doporučuji si o něm přečíst na Wikipedii nebo někde jinde.

Kódování q

G značí kódování kdy je text definován normálně, ale některé speciální znaky jsou kódovány jinak. Každý speciální znak je nahrazen sekvencí rovná se a HEX hodnotou znaku.

Příklady kódovaných hlaviček

Email s předmětem „Čeština“ lze zakódovat jak pomocí kódování q, tak b. Pokud bude kódován kódováním B bude hodnota hlavičky vypadat následovně:

=?UTF-8?B?xIxlxaF0aW5h?=

Pokud bude kódován pomocí kódování q, tak bude vypadat následovně.

=?utf-8?q?=C4=8Ce=C5=A1tina?=

Konfigurace aplikace

Kód aplikace začíná načítám knihoven pomocí příkazů preprocesoru #include. Dále jsou definovány konfigurační konstanty jako je název Wi-Fi, přes kterou se bude aplikace připojovat, heslo k této Wi-Fi, adresa poštovního serveru a uživatelské jméno a heslo k emailu.

Implementace aplikace

Aplikace na několika místech bude detekovat timeout. Pokud by server třeba neodpověděl aplikace se nesmí zaseknout. Pro tyto účely jsou v aplikaci vydefinovány makra pro inicializaci proměnný, kontrolu timeoutu, která se bude volat v cyklu a závěrečnou kontrolu, jestli timeout nastal. Protože proměnná může být jedna a může se stát (jakože se stane) že budeme potřebovat dvě kontroly do sebe zanořit jsou makra vydefinovány i pro druhou úroveň zanoření.

#include <_base64.h>
#include <SPI.h>
#include <Wire.h>
#include <WiFiUdp.h>
#include <WiFiServerSecure.h>
#include <WiFiServer.h>
#include <WiFiClientSecure.h>
#include <WiFiClient.h>
#include <ESP8266WiFiType.h>
#include <ESP8266WiFiSTA.h>
#include <ESP8266WiFiScan.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266WiFiGeneric.h>
#include <ESP8266WiFiAP.h>
#include <ESP8266WiFi.h>
#include <gfxfont.h>
#include <Adafruit_SPITFT_Macros.h>
#include <Adafruit_SPITFT.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>

// USER CONFIG HERE /
//#define DEBUG
#define pinLED 16
#define pinBTN 2
#define WiFiSsid "ssid"
#define WiFiPass "heslo"
#define pop3Server "pop3.seznam.cz"
#define pop3User "user jozin@zbazin.cz"  // DO NOT REMOVE user FROM BEGINING OF STRING. For username jozin@zbazin.cz type there (with quotes) "user jozin@zbazin.cz"
#define pop3Password "pass 1234" // DO NOT REMOVE pass FROM BEGINING OF STRING. For password 1234 type there (with quotes) "pass 1234"
// USER CONFIG HERE /

#define INIT_TIMEOUT int millisStart = millis();
#define CHECK_TIMEOUT_NOT_HAPPEND(DURATION) (millis() - millisStart < (DURATION))
#define CHECK_TIMEOUT_HAPPEND(DURATION, RETVAL_IF_HAPPEND) if (millis() - millisStart > (DURATION)) { /*Serial.println("TIMEOUT!");*/ return RETVAL_IF_HAPPEND; }

#define INIT_TIMEOUT2 int millisStart2 = millis();
#define CHECK_TIMEOUT_NOT_HAPPEND2(DURATION) (millis() - millisStart2 < (DURATION))
#define CHECK_TIMEOUT_HAPPEND2(DURATION, RETVAL_IF_HAPPEND) if (millis() - millisStart > (DURATION)) { /*Serial.println("TIMEOUT!");*/ return RETVAL_IF_HAPPEND; }

Následně jsou již definovány globální proměnné. Budeme potřebovat inicializovat knihovnu pro Nokia 5110 display, TCP spojení s POP3 serverem a nějaký odkládací buffer na data emailu. Protože budeme detekovat nový email a vizualizovat ho LEDkou, tak si musíme uložit ještě informace o tom, jestli nedošlo ke změně, respektive musíme si ukládat poslední známý email a každý další s ním porovnávat.

Adafruit_PCD8544 display = Adafruit_PCD8544(12, 5, 4);
WiFiClient tcpCon;
uint8_t buff[10000];

String lastDate;
String lastSubject;
String lastFrom;

#define LINE_BUFF_CAPACITY 100
char waitForAndReadLineBuff[LINE_BUFF_CAPACITY];

Dále v kódu následuje funkce setup, což je standardní funkce arduina. V ní inicializujeme Serial pro ladění, display, připojíme se k WiFi, nastavíme piny pro LED, tlačítko a nastavíme přerušení pro tlačítko (popis dále).

void setup() {
	Serial.begin(9600);
	Serial.println("rnInitialization started");
	delay(500);
	display.begin();
	display.setContrast(65);
	display.display();

	WiFi.mode(WIFI_STA);
	WiFi.disconnect();
	WiFi.begin(WiFiSsid, WiFiPass);
	waitForConnection(3);

	pinMode(pinLED, OUTPUT);
	pinMode(pinBTN, INPUT);
	digitalWrite(pinLED, 0);
	attachInterrupt(pinBTN, stopLED, FALLING);
}

Funkce setup obsahuje volání funkce waitForConnection(), která bude na display zobrazovat průběh připojování k Wi-Fi dokud se k ESP8266 k Wi-Fi nepřipojí. Pokud by se ESP k Wi-Fi během určitého časového intervalu k Wi-Fi nepřipojilo, tak zobrazí chybovou hlášku a ESP restartuje. Funkce opakuje animaci načítání několikrát, proto má parametr ttl, který určuje kolikrát se ještě bude animace zobrazovat. Ze setup() se volá s parametrem 3, po dokončení animace sama sebe zavolá s o jedna nižším parametrem (takže 2) a tak to dělá dokud parametr ttl nedojde k nule. Pak se zobrazí chyba, že se nepodařilo připojit k WiFi a ESP8266 se restartuje.

void waitForConnection(int ttl) {
	if (ttl == 0) {
		display.clearDisplay();
		display.display();
		display.println("WiFi failed");
		display.println("");
		display.println("Rebooting");
		display.display();
		delay(3000);
		ESP.restart();
		return;
	}
	int counter = 0;

	display.setTextSize(1);
	display.setTextColor(BLACK);
	display.setCursor(0, 0);
	display.clearDisplay();
	display.println("Connecting ...");
	display.drawRect(2, 20, display.width() - 4, 10, BLACK);
	display.display();

	while (WiFi.status() != WL_CONNECTED) {
		display.fillRect(2 + counter, 20, 1, 10, BLACK);
		display.display();
		counter++;
		if (counter > 80) {
			waitForConnection(ttl - 1);
			break;
		}
		delay(50);
	}

	display.clearDisplay();
	display.println("");
	display.println("");
	display.println("WiFi connected");
	display.display();
}

Notifikace příchozího emailu ESP8266

Program má samozřejmě krom funkce setup i funkci loop. V ní se volá funkce DownloadEmails, kde bude hlavní jádro komunikace s POP3 serverem a volá funkci delay() aby se emaily nestahovali moc často. Celý kód funkce je tedy následující.

void loop() {
	downloadEmails();
	delay(60000);
}

Funkce downloadEmails fakticky dělá to, co je popsáno v sekci o emailech. Vytvoří TCP spojení se serverem POP3 (port pro POP3 je 110). Počká na uvítací zprávu a +OK, obojí končí enterem. Pak odešle příkaz na přihlášení uživatele, pokud to neselže, tak odešle i uživatelovo heslo a pokud se vrátí to co se vrátit má (počet emailů), tak odešle příkaz na stažení emailu začne zpracovávat email, což už ale dělá jiná funkce. Funkce obsahuje několik debugovacích výpisů pro obsloužení chyb. Pro odeslání příkazu na server se volá funkce pop3Command, kterou si ukážeme dále. Funkce tedy vypadá následovně.

void downloadEmails() {
	tcpCon.connect(pop3Server, 110);
	String response = tcpCon.readStringUntil('r');
	tcpCon.readBytes(buff, 1);
	if (response.startsWith("+OK")) {
		pop3Command(pop3User);
		String response = pop3Command(pop3Password);
		if (!response.equals("")) { // Empty string is returned while timeout is thrown.
			String messageCount = response.substring(4, response.indexOf(' ', 4));
			messageCount.trim();

			if (messageCount.equals("")) {
#ifdef DEBUG
				Serial.print("Failed to auth to the POP3 server. Probably invalid username or password.");
#endif
				displayFailure();
			}

			String command = "retr " + messageCount + "rn";
			command.getBytes(buff, command.length(), 0);

			sendCommand((char*)buff);
			readEmail();
		}
		else {
#ifdef DEBUG
			Serial.print("Failed to auth to the POP3 server. Probably invalid username or password. RAW response from the server is: ");
			Serial.println(response);
#endif
			displayFailure();
		}
		tcpCon.stopAll();
	}
	else {

#ifdef DEBUG
		Serial.println("Failed to estabilish connection with POP3 server.");
#endif
		// TODO: Connction failed.
	}
}

Nyní se podívejme na funkci pop3Command.  Logika této funkce je ještě rozdělena do funkce sendCommand. Funkce pop3Command udělá vše, co je okolo příkazu potřeba (připraví spojení pro přenos dat, tzn. přečte všechny nepřečtené znaky a pak přečte a naparsuje odpověď). Funkce sendCommand pak nedělá nic jiného, než jenom pošle příkaz na server. Funkce čeká na právě 3 bajty, podle kterých pozná že server odpověděl +OK nebo chybu (důležité je že pozná že server alespoň něco odpověděl). V průběhu samozřejmě pomocí maker kontroluje timeout. Potom co tohle zpracuje tak vrátí přímo odpověď ze serveru, bez ohledu na to, jestli je odpověď +OK nebo ne. Obě funkce vypadají následovně.

/*
	Executes an POP3 command at the server. This function returns output from the server
*/
String pop3Command(char* command) {
	sendCommand(command);
	INIT_TIMEOUT;
	while (tcpCon.available() < 3 && CHECK_TIMEOUT_NOT_HAPPEND(10000)) {
		delay(1);
	}
	CHECK_TIMEOUT_HAPPEND(10000, "")
	int avail = tcpCon.available();

#ifdef DEBUG
	Serial.println(String(avail));
#endif
	tcpCon.readBytes(buff, avail);
	String response = String((char*)buff);
	return response;
}

/*
	@internal
	Executes an command on the POP3 server. This function only do required stuff to send the command to the server. It does not process output anyway.
*/
void sendCommand(char* command) {

#ifdef DEBUG
	Serial.print("CMD: ");
	Serial.println(command);
	Serial.println("WASTE: " + String(tcpCon.available()) + " bytes");
#endif
	INIT_TIMEOUT;
	while (tcpCon.available() && CHECK_TIMEOUT_NOT_HAPPEND(10000)) {
		tcpCon.read();
	}
	CHECK_TIMEOUT_HAPPEND(10000, );
	tcpCon.write(command);
	tcpCon.write("rn");
}

Zatím jsme přeskočili jednu funkci a to funkci, která parsuje stažený email. Jedná se o funkci readEmail. Tato funkce silně závisí na funkci waitForAndReadLine(), která se stará o stažení právě jedné řádky z TCP spojení. Tato funkce prostě čeká, než proudem přijdou nějaká data a čte je, dokud v proudu nenarazí na sekvenci zalomení řádku. Řádky jsou e emailů nesmírně důležité, protože mají několik speciálních významů. Slouží jednak jako oddělovače jednotlivých hlaviček emailů. Dva po sobě jdoucí zalomení řádku pak oddělují hlavičky od obsahu. Funkce waitForAndReadLine vypadá následovně. Ve funkci si můžete všimnout využití zanořené kontroly timeoutu a kontrolu přetečení bufferu (na což někteří programátoři zapomínají a při tom se fakticky jedná o bezpečnostní riziko).

/*
	Reads one line from TCP stream to POP3 server. Returns it. Output does not contains rn.
*/
String waitForAndReadLine() {
	int buffOffset = 0;
	INIT_TIMEOUT;
	while (CHECK_TIMEOUT_NOT_HAPPEND(10000)) {
		INIT_TIMEOUT2;
		while (!tcpCon.available() && CHECK_TIMEOUT_NOT_HAPPEND2(10000)) {
			delay(1);
		}
		CHECK_TIMEOUT_HAPPEND2(10000, "")

		for (int i = 0; i < tcpCon.available() && buffOffset < LINE_BUFF_CAPACITY; i++) {
			char byte[1];
			tcpCon.readBytes(byte, 1);

			if (byte[0] == 'r') {
				waitForAndReadLineBuff[buffOffset] = '�';
				return String(waitForAndReadLineBuff);
			}
			else {
				waitForAndReadLineBuff[buffOffset++] = byte[0];
			}
		}


		if (buffOffset == LINE_BUFF_CAPACITY) {
			// Buffer is full, waste all next bytes until r

			char byte = '�';
			while (byte != 'r') {
				INIT_TIMEOUT2;
				while (!tcpCon.available() && CHECK_TIMEOUT_NOT_HAPPEND2(10000)) {
					delay(1);
				}
				CHECK_TIMEOUT_HAPPEND2(10000, "");
				tcpCon.readBytes(&byte, 1);
			}

			return String(waitForAndReadLineBuff);
		}
	}
	CHECK_TIMEOUT_HAPPEND(10000, "");
}

Vraťme se k dosud nepopsané funkci readEmail(). Tato roztomilá funkce přečte všechny řádky emailu od shora dolů a najde v nich podstatné informace, které ji zajímají. Náš program zajímají hlavičky Date, From a Subject. Kontroluje to v cyklu, kde testuje, jestli první znak řádku není tečka. Řádkem s tečkou na začátku končí každý email. Jinými slovy cykl přečte celý email, řádek po řádku. Funkce při zapnutém debuging data emailu vypíše na Seriál a zkontroluje, jestli se shodují s předchozím uloženým emailem v globálních proměnných. Pokud se neshodují považují to za nový email, který nechá zobrazit na display a rozsvítí LEDku. Za zmínku stojí, že při ukládání hodnot nového emailu do proměnných neprobíhá napřímo, ale „posílají“ se jako vstup do dále popsaných funkcí, které je nějakým způsobem zpracují už jen z toho důvodu, že hodnota může být zakódována pomocí RFC 2047 jak bylo zmíněno v teoretické sekci o emailech. Celá funkce vypadá následovně.

/*
	Handles TCP connection to POP3 server and parses email from that.
*/
void readEmail() {

#ifdef DEBUG
	Serial.println("READING EMAIL");
#endif
	String line = waitForAndReadLine();
	line.trim();

	String from;
	String subject;
	String date;

	INIT_TIMEOUT;
	while (line[0] != '.' && CHECK_TIMEOUT_NOT_HAPPEND(10000)) { // timeout 10sec
		line = waitForAndReadLine();
		line.trim();

		if (line.startsWith("From: ")) {
			from = fixCzechCharacters(processFrom(line.substring(6)));
		}

		if (line.startsWith("Subject: ")) {
			subject = fixCzechCharacters(decodeRfc2047(line.substring(9)));
		}

		if (line.startsWith("Date: ")) {
			date = line.substring(6);
		}
	}
	CHECK_TIMEOUT_HAPPEND(10000, )

#ifdef DEBUG
	Serial.println("=========================================");
	Serial.println("DATE=" + date);
	Serial.println("FROM=" + from);
	Serial.println("SUBJECT=" + subject);
	Serial.println("=========================================");
#endif
	if (!lastDate.equals(date) || !lastFrom.equals(from) || !lastSubject.equals(subject)) {
		lastDate = date;
		lastFrom = from;
		lastSubject = subject;

		digitalWrite(pinLED, 1);
		showOnDisplay(from, subject, date);
	}

}

Než se pustíme do pomocných funkcí pro „úpravy“ hlaviček z emailu, tak si ukážeme jednoduchou funkci showOnDisplay, která zobrazuje nově načtený email z funkce readEmail.

void showOnDisplay(String from, String subject, String date) {
	display.clearDisplay();
	display.setTextSize(1);
	display.setCursor(0, 0);
	display.println("Od:" + from);
	display.println("Predmet:" + subject);
	display.display();
}

První popisovanou funkcí na dekódování vstupních hlaviček bude funkce decodeRfc2047. Tato funkce umí dekódovat hodnotu podle RFC 2407 (koneckonců má to v názvu). První věc, kterou funkce kontroluje je, jestli je hodnota vůbec kódována podle RFC 2407. Pokud nezačíná hodnota sekvencí =?, tak se vrátí přímo vstupní hodnota. Pokud hodnota touto sekvencí začíná, tak se přeskočí kódování, protože to na Arduino dost dobře nejsme schopni zpracovat. Podporujeme pouze UTF-8, protože v tomto kódování je i soubor zdrojového kódu (alespoň ten můj, pokud svůj zdroják máte v jiném kódování, tak to doporučuji na UTF-8 převést, za jiného kódování Vám nebude fungovat funkce fixCzechCharacters popisována dále). Po kódování přečteme metodu kódování, což může být buď b nebo q, případě to může být velkými písmeny. Podle toho budeme řetězec dále zpracovávat.

Pokud se jedná o B, využijeme knihovny pro Base64. Knihovna nepodporuje Arduino String, takže musíme připravit buffery a string převést na C string a předat ho knihovně jako pointer. Musíme samozřejmě opět kontrolovat velikost bufferu a zabránit jejímu přetečení.

Pokud se jedná o Q, tak nás v řetězci zajímají sekvence začínající pomocí znaku rovná se. V řetězci je budeme vyhledávat tak dlouho dokud tam nějaký bude a vždy první v řetězci nahradíme tím správným znakem. Jakmile nahradíme první tak pro příští iteraci první bude ve skutečnosti na vstupu druhý, v další iteraci třetí, atd., takže tímto algoritmem je prostě dřív či později nahradíme všechny. Algoritmus tedy najde rovná se + 2 další znaky, což j tex HEX kód nahrazovaného znaku, pomocí C funkce strtol převede HEX řetězec na správné číslo. Protože strtol je C funkce, tak opět musíme Arduino string převést do bufferu a pro nahrazení řetězce ve vstupu využijeme arduino funkce, což ale znamená že výstup z C funkce (číslo) musíme vytvořit řetězec (zatím C řetězec) a ten převést na Arduino řetězec.

To poslední, co funkce dělá před každým navrácením hodnoty (i kdyby hodnotu nijak nedekódovala) je nahrazení podtržítka za mezeru. To je zvláštnost, kterou RFC 2407 zavádí a platí vždy v každém emailu – podtržítko je (byl) zakázaný znak a pokud má být vidět musí se ošetřit (to ale mi řešit nebudeme).

Celá funkce vypadá následovně.

/*
	Parse subject value as described in RFC 2047 https://www.ietf.org/rfc/rfc2047.txt

	For example: if original subject is =?utf-8?q?=C4=8Ce=C5=A1tina?= then output is Čeština
	For example: if original subject is =?UTF-8?B?xIxlxaF0aW5h?= then output is Čeština
*/
String decodeRfc2047(String originalSubject) {
	if (originalSubject.startsWith("=?")) {
		String temp = originalSubject.substring(2);
		temp = temp.substring(temp.indexOf("?") + 1); // skip encoding
		char format = temp.charAt(0);
		String data = temp.substring(2, temp.lastIndexOf("?="));
		String output;

		if (format == 'q' || format == 'Q') {
			while (data.indexOf("=") != -1) {
				String code = data.substring(data.indexOf("=") + 1, data.indexOf("=") + 3);
				char buff[3];
				code.toCharArray(buff, 3, 0);
				buff[2] = '�';
				unsigned char number = (unsigned char)strtol(buff, NULL, 16);
				buff[0] = number;
				buff[1] = '�';
				data.replace("=" + code, String(buff));
			}
			output = data;
		}
		else if (format == 'b' || format == 'B') {
			char decoded[301];
			char input[300];
			int toCopy = 300;

			memset(decoded, 0, 301);
			if ((data.length() + 1) < toCopy) {
				toCopy = data.length() + 1;
			}
			data.toCharArray(input, toCopy);
			b64_decode(decoded, input, toCopy);
			output = String(decoded);
		}
		else {
			output = "";
		}
		output.replace("_", " ");
		return output;
	}
	else {
		originalSubject.trim();
		originalSubject.replace("_", " ");
		return originalSubject;
	}
}

Další speciální funkcí (která na konci volá popsanou decodeRfc2047) je funkce parseFrom, která se volá na naparsování hlavičky From. Emaily umožňují, že si můžete vyplnit svoje jméno příjmení. Díky toho se pak místo vaší magické (a značně ulítlé) adresy johny.lol-boss1464165@centrum.cz (pozn. Autor projektu má ve skutečnosti značně ulítlejší adresu), kterou jste si v dětství založily bude zobrazovat Vaše jméno a příjmení. Funguje to tak, že se v hlavičce pošle obojí dvoje. Email se zpravidla uvádí mezi < a > a jméno před tím. Pokud má jméno a příjmení mezeru (jakože mezi jménem a příjmením mezera je), tak je to uvedeno v uvozovkách. Uvozovky tam nejsou je v případě, že jste si vyplnili jen jedno. Hodnota, která v hlavičce From přijde může vypadat třeba následovně:

"Emil Psal" <johny.lol-boss1464165@centrum.cz>

Cílem této funkce je z takového řetězce vrátit tu nejpodstatnější informaci. Pokud má uživatel vyplněné jméno, příjmení nebo obojí, tak vrátí to (bez uvozovek, pokud by tam byli). Pokud to tam nemá, tak vrátí emailovou adresu bez < a >. Funkce z řetězce odstraní úvodní a koncové bílé znaky a pokud je hodnota dekódována pomocí RFC 2407, tak to dekóduje. Celá funkce vypadá následovně.

/*
	Parses probably the most usefull part from FROM header. Email format could be

	"Jožin Zbažin" <jozin@zbazin.cz>

	but could miss osme parts. For example 

	<jozin@zbazin.cz>

	is still valid.
*/
String processFrom(String originalFrom) {

#ifdef DEBUG
	Serial.print("Procesing from: ");
	Serial.println(originalFrom);
#endif
	String output;
	if (originalFrom.startsWith("<")) {
		output = originalFrom.substring(1, originalFrom.indexOf(">"));
	}
	else if (originalFrom.startsWith(""")) {
		String temp = originalFrom.substring(1);
		output = temp.substring(0, temp.indexOf("""));
	}
	else if (originalFrom.indexOf("<") != -1) {
		output = originalFrom.substring(0, originalFrom.indexOf("<"));
	}
	else {
		output = originalFrom;
	}
	output.trim();
	return decodeRfc2047(output);
}

Poslední ze sady upravovacích funkcí je funkce fixCzechCharacters, která nahradí české znaky s diakritikou za znaky bez ní. Zde je třeba dodat, že je důležité kódování zdrojového kódu. Funkce bude umět nahrazovat pouze v kódování stejném jako je kódování zdrojového kódu. Pro nejobecnější použití je třeba mít zdrojový kód v kódování UTF-8, což je světově uznávaný standard, který podporuje písmena a znaky všech abeced všech států na světě. Funkce vypadá následovně:

/*
	Removes special behaviour of some czech characters. It override input data and also returns it.

	For example: if data is Čeština then data would be Cestina
*/
String fixCzechCharacters(String data) {
	data.replace("Ě", "E");
	data.replace("Š", "S");
	data.replace("Č", "C");
	data.replace("Ř", "R");
	data.replace("Ž", "Z");
	data.replace("Ý", "Y");
	data.replace("Á", "A");
	data.replace("Í", "I");
	data.replace("É", "E");
	data.replace("Ů", "U");
	data.replace("Ú", "U");
	data.replace("Ď", "D");
	data.replace("Ť", "T");
	data.replace("Ň", "N");
	data.replace("ě", "e");
	data.replace("š", "s");
	data.replace("č", "c");
	data.replace("ř", "r");
	data.replace("ž", "z");
	data.replace("ý", "y");
	data.replace("á", "a");
	data.replace("í", "i");
	data.replace("é", "e");
	data.replace("ů", "u");
	data.replace("ú", "u");
	data.replace("ď", "d");
	data.replace("ť", "t");
	data.replace("ň", "n");
	return data;
}

Ve funkci pro obsluhu protokolu POP3 se v případě selhání volá doposud nepopsaná funkce displayFailure, která krom vypsání chyby na display zároveň nekonečným cyklem zastaví další provádění programu.

void displayFailure() {
	display.clearDisplay();
	display.display();
	display.println("rnError occured!");
	display.println("");
#ifndef DEBUG
	display.println("Enable DEBUG");
#endif
	display.println("See serial");
	display.display();

	while (1) {
		delay(1);
	}
}

Poslední nepopsaná funkce, která je obsluhou přerušení tlačítka je funkce, která zhasne LED, pokud uživatel klikne na tlačítko. Logika je taková, že když přijde email, tak se rozsvítí, jakmile si toho uživatel všimne, zmáčkne tlačítko a LED zhasne. Jakmile přijde další email LED se opět rozsvítí.

void stopLED() {
	digitalWrite(pinLED, 0);
}

Závěr

V tomto projektu jsem (se snažil) ukázat že tvorba aplikace komunikující přes síť není složitá a fakticky ani na spoustu protokolů nepotřebuje žádnou knihovnu, protože není moc těžké si logiku té „knihovny“ vytvořit.

Aplikace emaily zobrazuje na display, ale není moc těžké ji upravit, aby na základě příchozího emailu dělala něco jiného. Ve funkci, která zobrazuje email na display si tak můžete napsat jednoduchý if na předmět a pokud bude předmět obsahovat nějaký specifický řetězec můžete provést nějakou akci. Pokud nemáte display (nebo máte jiný display), tak si můžete řádky pro práci s displejem smazat a například napsat svoji vlastní logiku. Aplikace, kde jde vidět odeslání jednoho emailu a jeho indikaci (rozsvícení LED) a vypsání na display na videu níže.

Kompletní balík zdrojových kódů a použitých knihoven je k dispozici zde.

Seznam součástek

FB gp tw

Další podobné články