├── ESP8266-Gmail-Noot ├── ESP8266-Gmail-Noot.ino ├── README.md └── data │ └── config.json ├── ESP8266-OAUTH2 └── ESP8266-OAUTH2.ino ├── LICENSE └── README.md /ESP8266-Gmail-Noot/ESP8266-Gmail-Noot.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | 6 | /* 7 | * ============ Variables ============ 8 | */ 9 | 10 | //#define DEBUG true 11 | #define LED_PIN 2 // built-in LED 12 | 13 | // WiFi Setup 14 | String wifi_ssid = ""; 15 | String wifi_pass = ""; 16 | 17 | // OAUTH2 credentials 18 | String client_id = ""; 19 | String client_secret = ""; 20 | 21 | // Tokens 22 | String access_token = ""; 23 | String refresh_token = ""; 24 | 25 | // Poll settings 26 | const int POLL_INTERVAL = 10; 27 | unsigned long POLL_MILLIS = 0; 28 | 29 | /* 30 | * ============ OAUTH2 ============ 31 | */ 32 | 33 | // SSL Setup 34 | // http://askubuntu.com/questions/156620/how-to-verify-the-ssl-fingerprint-by-command-line-wget-curl/ 35 | // echo | openssl s_client -connect www.googleapis.com:443 | openssl x509 -fingerprint -noout 36 | const char* host = "www.googleapis.com"; 37 | const int httpsPort = 443; 38 | const char* fingerprint1 = "A6 7A 38 10 2C 29 27 9F F5 91 52 92 49 F2 2A E7 C0 B4 20 A8"; 39 | const char* fingerprint2 = "39 F2 B5 65 4F C9 E2 EF 46 F1 8E BC 66 15 E2 72 79 20 94 46"; 40 | 41 | // OAUTH2 Basics 42 | String access_type = "offline"; 43 | String redirect_uri = "urn:ietf:wg:oauth:2.0:oob"; 44 | String response_type = "code"; 45 | String auth_uri = "https://accounts.google.com/o/oauth2/auth"; 46 | String info_uri = "/oauth2/v3/tokeninfo"; 47 | String token_uri = "/oauth2/v4/token"; 48 | String authorization_code = ""; // leave empty 49 | 50 | // Send messages only. No read or modify privileges on mailbox. 51 | String gmail_scope = "https://www.googleapis.com/auth/gmail.readonly"; 52 | // required to determine which user really authenticated 53 | String scope = "email " + gmail_scope; 54 | 55 | static const int ERROR_STATE = -1; 56 | static const int INITIAL_STATE = 0; 57 | static const int AWAIT_CHALLANGE = 1; 58 | static const int EXCHANGING = 2; 59 | static const int INFO = 3; 60 | static const int REFRESHING = 4; 61 | static const int DO_IT = 5; 62 | static const int END_STATE = 6; 63 | 64 | // Set global variable attributes. 65 | static int CURRENT_STATE = INITIAL_STATE; 66 | 67 | String parseResponse(String response, String key) { 68 | DynamicJsonBuffer jsonBuffer; 69 | JsonObject& root = jsonBuffer.parseObject(response); 70 | 71 | if (!root.success()) { 72 | Serial.println("parseObject() failed"); 73 | return ""; 74 | } else { 75 | return root[key]; 76 | String output; 77 | root.printTo(output); 78 | Serial.print("JSON: "); Serial.println(output); 79 | return output; 80 | } 81 | } 82 | 83 | String getRequest(const char* server, String request) { 84 | #ifdef DEBUG 85 | Serial.print("Function: "); Serial.println("getRequest()"); 86 | #endif 87 | 88 | String result = ""; 89 | 90 | // Use WiFiClientSecure class to create TLS connection 91 | WiFiClientSecure client; 92 | 93 | #ifdef DEBUG 94 | Serial.print("Connecting to: "); Serial.println(server); 95 | #endif 96 | 97 | if (!client.connect(server, httpsPort)) { 98 | Serial.println("connection failed"); 99 | return result; 100 | } 101 | 102 | if (client.verify(fingerprint1, server) || client.verify(fingerprint2, server)) { 103 | #ifdef DEBUG 104 | Serial.println("certificate matches"); 105 | Serial.print("get: "); Serial.println(request); 106 | #endif 107 | 108 | client.print(request); 109 | 110 | #ifdef DEBUG 111 | Serial.println("request sent"); 112 | Serial.println("Receiving response"); 113 | #endif 114 | 115 | while (client.connected()) { 116 | if(client.find("HTTP/1.1 ")) { 117 | String status_code = client.readStringUntil('\r'); 118 | #ifdef DEBUG 119 | Serial.print("Status code: "); Serial.println(status_code); 120 | #endif 121 | if(status_code == "401 Unauthorized") { 122 | // lets see if access_token expired 123 | CURRENT_STATE = INFO; 124 | } else if(status_code != "200 OK") { 125 | Serial.println("There was an error"); 126 | break; 127 | } 128 | } 129 | if(client.find("\r\n\r\n")) { 130 | String line = client.readStringUntil('\r'); 131 | #ifdef DEBUG 132 | Serial.println("Data:"); 133 | Serial.println(line); 134 | #endif 135 | result += line; 136 | } 137 | } 138 | #ifdef DEBUG 139 | Serial.println("closing connection"); 140 | #endif 141 | return result; 142 | } else { 143 | Serial.println("certificate doesn't match"); 144 | } 145 | return result; 146 | } 147 | 148 | 149 | String postRequest(const char* server, String header, String data) { 150 | #ifdef DEBUG 151 | Serial.print("Function: "); Serial.println("postRequest()"); 152 | #endif 153 | 154 | String result = ""; 155 | 156 | // Use WiFiClientSecure class to create TLS connection 157 | WiFiClientSecure client; 158 | #ifdef DEBUG 159 | Serial.print("Connecting to: "); Serial.println(server); 160 | #endif 161 | 162 | if (!client.connect(server, httpsPort)) { 163 | Serial.println("connection failed"); 164 | return result; 165 | } 166 | 167 | if (client.verify(fingerprint1, server) || client.verify(fingerprint2, server)) { 168 | #ifdef DEBUG 169 | Serial.println("certificate matches"); 170 | Serial.print("post: "); Serial.println(header + data); 171 | #endif 172 | 173 | client.print(header + data); 174 | 175 | #ifdef DEBUG 176 | Serial.println("request sent"); 177 | Serial.println("Receiving response"); 178 | #endif 179 | 180 | while (client.connected()) { 181 | if(client.find("HTTP/1.1 ")) { 182 | String status_code = client.readStringUntil('\r'); 183 | #ifdef DEBUG 184 | Serial.print("Status code: "); Serial.println(status_code); 185 | #endif 186 | if(status_code != "200 OK") { 187 | Serial.println("There was an error"); 188 | break; 189 | } 190 | } 191 | if(client.find("\r\n\r\n")) { 192 | String line = client.readStringUntil('\r'); 193 | #ifdef DEBUG 194 | Serial.println("Data:"); 195 | Serial.println(line); 196 | #endif 197 | result += line; 198 | } 199 | } 200 | 201 | #ifdef DEBUG 202 | Serial.println("closing connection"); 203 | #endif 204 | return result; 205 | } else { 206 | Serial.println("certificate doesn't match"); 207 | } 208 | return result; 209 | } 210 | 211 | // create URL 212 | void authorize() { 213 | #ifdef DEBUG 214 | Serial.print("Function: "); Serial.println("authorize()"); 215 | #endif 216 | if (refresh_token == "") { 217 | String URL = auth_uri + "?"; 218 | URL += "scope=" + urlencode(scope); 219 | URL += "&redirect_uri=" + urlencode(redirect_uri); 220 | URL += "&response_type=" + urlencode(response_type); 221 | URL += "&client_id=" + urlencode(client_id); 222 | URL += "&access_type=" + urlencode(access_type); 223 | Serial.println("Goto URL: "); 224 | Serial.println(URL); Serial.println(); 225 | Serial.print("Enter code: "); 226 | CURRENT_STATE = AWAIT_CHALLANGE; 227 | } else { 228 | CURRENT_STATE = INFO; 229 | } 230 | } 231 | 232 | bool exchange() { 233 | #ifdef DEBUG 234 | Serial.print("Function: "); Serial.println("exchange()"); 235 | #endif 236 | 237 | if (authorization_code != "") { 238 | 239 | String postData = ""; 240 | postData += "code=" + authorization_code; 241 | postData += "&client_id=" + client_id; 242 | postData += "&client_secret=" + client_secret; 243 | postData += "&redirect_uri=" + redirect_uri; 244 | postData += "&grant_type=" + String("authorization_code"); 245 | 246 | String postHeader = ""; 247 | postHeader += ("POST " + token_uri + " HTTP/1.1\r\n"); 248 | postHeader += ("Host: " + String(host) + ":" + String(httpsPort) + "\r\n"); 249 | postHeader += ("Connection: close\r\n"); 250 | postHeader += ("Content-Type: application/x-www-form-urlencoded\r\n"); 251 | postHeader += ("Content-Length: "); 252 | postHeader += (postData.length()); 253 | postHeader += ("\r\n\r\n"); 254 | 255 | String result = postRequest(host, postHeader, postData); 256 | 257 | CURRENT_STATE = END_STATE; 258 | return true; 259 | } else { 260 | return false; 261 | } 262 | } 263 | 264 | void refresh() { 265 | #ifdef DEBUG 266 | Serial.print("Function: "); Serial.println("refresh()"); 267 | #endif 268 | if (refresh_token != "") { 269 | 270 | String postData = ""; 271 | postData += "refresh_token=" + refresh_token; 272 | postData += "&client_id=" + client_id; 273 | postData += "&client_secret=" + client_secret; 274 | postData += "&grant_type=" + String("refresh_token"); 275 | 276 | String postHeader = ""; 277 | postHeader += ("POST " + token_uri + " HTTP/1.1\r\n"); 278 | postHeader += ("Host: " + String(host) + ":" + String(httpsPort) + "\r\n"); 279 | postHeader += ("Connection: close\r\n"); 280 | postHeader += ("Content-Type: application/x-www-form-urlencoded\r\n"); 281 | postHeader += ("Content-Length: "); 282 | postHeader += (postData.length()); 283 | postHeader += ("\r\n\r\n"); 284 | 285 | String result = postRequest(host, postHeader, postData); 286 | access_token = parseResponse(result, String("access_token")); 287 | #ifdef DEBUG 288 | Serial.print("access_token: "); Serial.println(access_token); 289 | #endif 290 | if(access_token != "") { 291 | CURRENT_STATE = DO_IT; 292 | } else { 293 | CURRENT_STATE = END_STATE; 294 | } 295 | } 296 | } 297 | 298 | bool info() { 299 | #ifdef DEBUG 300 | Serial.print("Function: "); Serial.println("info()"); 301 | #endif 302 | if (access_token != "") { 303 | 304 | String reqHeader = ""; 305 | reqHeader += ("GET " + info_uri + "?access_token=" + access_token + " HTTP/1.1\r\n"); 306 | reqHeader += ("Host: " + String(host) + ":" + String(httpsPort) + "\r\n"); 307 | reqHeader += ("Connection: close\r\n"); 308 | reqHeader += ("\r\n\r\n"); 309 | String result = getRequest(host, reqHeader); 310 | // need to check for valid token here 311 | // Look for expires_in 312 | String expires_in = parseResponse(result, String("expires_in")); 313 | Serial.print("expires_in: "); Serial.println(expires_in); 314 | if(expires_in.toInt() >= 300) { // got 5 min.? 315 | CURRENT_STATE = DO_IT; 316 | } else { 317 | CURRENT_STATE = REFRESHING; 318 | } 319 | } else { 320 | CURRENT_STATE = REFRESHING; 321 | return false; 322 | } 323 | return true; 324 | } 325 | 326 | /* 327 | * ============ Filesystem ============ 328 | */ 329 | 330 | void cleanFS() { 331 | //clean FS, for testing 332 | Serial.println("formatting file system"); 333 | SPIFFS.format(); 334 | Serial.println("done."); 335 | } 336 | 337 | bool writeConfig() { 338 | bool result = false; 339 | if (SPIFFS.begin()) { 340 | Serial.println("mounted file system"); 341 | 342 | Serial.println("open config file for writing"); 343 | File configFile = SPIFFS.open("/config.json", "w+"); 344 | 345 | if (configFile) { 346 | Serial.println("opened config file"); 347 | DynamicJsonBuffer jsonBuffer; 348 | JsonObject& jsonConfig = jsonBuffer.createObject(); 349 | jsonConfig["wifi_ssid"] = wifi_ssid; 350 | jsonConfig["wifi_pass"] = wifi_pass; 351 | jsonConfig["client_id"] = client_id; 352 | jsonConfig["client_secret"] = client_secret; 353 | jsonConfig["refresh_token"] = refresh_token; 354 | jsonConfig.prettyPrintTo(Serial); Serial.println(); 355 | jsonConfig.printTo(configFile); 356 | result = true; 357 | } 358 | 359 | Serial.println("closing config file"); 360 | configFile.close(); 361 | 362 | } else { 363 | Serial.println("failed to mount FS"); 364 | } 365 | Serial.println("unmounting file system"); 366 | SPIFFS.end(); 367 | return result; 368 | } 369 | 370 | bool readConfig() { 371 | 372 | bool result = false; 373 | 374 | if (SPIFFS.begin()) { 375 | Serial.println("mounted file system"); 376 | if (SPIFFS.exists("/config.json")) { 377 | //file exists, reading and loading 378 | Serial.println("reading config file"); 379 | File configFile = SPIFFS.open("/config.json", "r"); 380 | if (configFile) { 381 | Serial.println("opened config file"); 382 | size_t size = configFile.size(); 383 | 384 | // Allocate a buffer to store contents of the file. 385 | std::unique_ptr buf(new char[size]); 386 | configFile.readBytes(buf.get(), size); 387 | DynamicJsonBuffer jsonBuffer; 388 | JsonObject& jsonConfig = jsonBuffer.parseObject(buf.get()); 389 | 390 | if (jsonConfig.success()) { 391 | Serial.println("config parsed successfully"); 392 | jsonConfig.prettyPrintTo(Serial); Serial.println(); 393 | 394 | wifi_ssid = jsonConfig["wifi_ssid"].as(); 395 | wifi_pass = jsonConfig["wifi_pass"].as(); 396 | client_id = jsonConfig["client_id"].as(); 397 | client_secret = jsonConfig["client_secret"].as(); 398 | refresh_token = jsonConfig["refresh_token"].as(); 399 | 400 | result = true; 401 | } else { 402 | Serial.println("failed to load json config"); 403 | } 404 | } 405 | Serial.println("closing config file"); 406 | configFile.close(); 407 | } else { 408 | Serial.println("No config file found."); 409 | } 410 | } else { 411 | Serial.println("failed to mount FS"); 412 | } 413 | Serial.println("unmounting file system"); 414 | SPIFFS.end(); 415 | return result; 416 | } 417 | 418 | /* 419 | * ============ Gmail Noot ============ 420 | */ 421 | 422 | bool getUnreadCount() { 423 | // GET https://www.googleapis.com/gmail/v1/users/userId/threads 424 | 425 | if(millis() - POLL_MILLIS >= POLL_INTERVAL * 1000UL || POLL_MILLIS == 0) { 426 | String reqHeader = ""; 427 | reqHeader += ("GET /gmail/v1/users/me/threads?"); 428 | reqHeader += ("access_token=" + access_token); 429 | reqHeader += ("&labelIds=INBOX"); 430 | reqHeader += ("&q=is:unread"); 431 | reqHeader += ("&fields=resultSizeEstimate"); 432 | reqHeader += (" HTTP/1.1\r\n"); 433 | reqHeader += ("Host: " + String(host) + ":" + String(httpsPort) + "\r\n"); 434 | reqHeader += ("Connection: close\r\n"); 435 | reqHeader += ("\r\n\r\n"); 436 | 437 | String result = getRequest(host, reqHeader); 438 | String unread = parseResponse(result, String("resultSizeEstimate")); 439 | 440 | #ifdef DEBUG 441 | Serial.print("unread: "); Serial.println(unread); 442 | #endif 443 | 444 | if(unread.toInt() > 0) { // you got mail. 445 | Serial.print("You got "); Serial.print(unread); Serial.println(" unread mail thread(s) in your inbox."); 446 | digitalWrite(LED_PIN, LOW); // noot noot 447 | } else { 448 | digitalWrite(LED_PIN, HIGH); 449 | } 450 | 451 | POLL_MILLIS = millis(); 452 | return true; 453 | } else { 454 | return false; 455 | } 456 | } 457 | 458 | /* 459 | * ============ General ============ 460 | */ 461 | 462 | String urlencode(String str) 463 | { 464 | String encodedString = ""; 465 | char c; 466 | char code0; 467 | char code1; 468 | char code2; 469 | for (int i = 0; i < str.length(); i++) { 470 | c = str.charAt(i); 471 | if (c == ' ') { 472 | encodedString += '+'; 473 | } else if (isalnum(c)) { 474 | encodedString += c; 475 | } else { 476 | code1 = (c & 0xf) + '0'; 477 | if ((c & 0xf) > 9) { 478 | code1 = (c & 0xf) - 10 + 'A'; 479 | } 480 | c = (c >> 4) & 0xf; 481 | code0 = c + '0'; 482 | if (c > 9) { 483 | code0 = c - 10 + 'A'; 484 | } 485 | code2 = '\0'; 486 | encodedString += '%'; 487 | encodedString += code0; 488 | encodedString += code1; 489 | //encodedString+=code2; 490 | } 491 | yield(); 492 | } 493 | return encodedString; 494 | } 495 | 496 | // enable Serial communication 497 | String serialComm() { 498 | String result = ""; 499 | while (Serial.available()) { 500 | String inputString = Serial.readString(); 501 | inputString.trim(); 502 | if (inputString == "clean") { 503 | cleanFS(); 504 | } else if (inputString == "read") { 505 | readConfig(); 506 | } else if (inputString == "write") { 507 | writeConfig(); 508 | } else { 509 | result = inputString; 510 | } 511 | } 512 | return result; 513 | } 514 | 515 | void setup() { 516 | Serial.begin(115200); Serial.println(); 517 | pinMode(LED_PIN, OUTPUT); 518 | // read config from file system 519 | readConfig(); 520 | if(wifi_ssid != "") { 521 | Serial.print("Connecting to: "); Serial.println(wifi_ssid); 522 | char ssidBuf[wifi_ssid.length()+1]; 523 | wifi_ssid.toCharArray(ssidBuf, wifi_ssid.length()+1); 524 | char passBuf[wifi_pass.length()+1]; 525 | wifi_pass.toCharArray(passBuf, wifi_pass.length()+1); 526 | WiFi.begin(ssidBuf, passBuf); 527 | while (WiFi.status() != WL_CONNECTED) { 528 | delay(500); 529 | Serial.print("."); 530 | } 531 | Serial.println(); 532 | Serial.print("WiFi connected. "); Serial.print("IP address: "); Serial.println(WiFi.localIP()); 533 | Serial.println(); 534 | } 535 | } 536 | 537 | void loop() { 538 | serialComm(); 539 | switch (CURRENT_STATE) { 540 | case INITIAL_STATE: 541 | authorize(); 542 | break; 543 | case AWAIT_CHALLANGE: 544 | authorization_code = serialComm(); 545 | if (authorization_code != "") { 546 | CURRENT_STATE = EXCHANGING; 547 | } 548 | break; 549 | case EXCHANGING: 550 | exchange(); 551 | break; 552 | case INFO: 553 | info(); 554 | break; 555 | case REFRESHING: 556 | refresh(); 557 | break; 558 | case DO_IT: 559 | getUnreadCount(); 560 | break; 561 | case END_STATE: 562 | break; 563 | default: 564 | Serial.println("ERROR"); 565 | break; 566 | } 567 | } 568 | -------------------------------------------------------------------------------- /ESP8266-Gmail-Noot/README.md: -------------------------------------------------------------------------------- 1 | This is a PoC to demonstrate how to poll your Gmail Inbox for unread mail threads and make an LED light-up accordingly. 2 | -------------------------------------------------------------------------------- /ESP8266-Gmail-Noot/data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "wifi_ssid": "", 3 | "wifi_pass": "", 4 | "client_id": "", 5 | "client_secret": "", 6 | "refresh_token": "" 7 | } 8 | -------------------------------------------------------------------------------- /ESP8266-OAUTH2/ESP8266-OAUTH2.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | /* 7 | * ===================================== 8 | * Start editing your configuration here 9 | */ 10 | 11 | //#define DEBUG true 12 | 13 | // WiFi Setup 14 | const char* ssid = "wireless ssid goes here"; 15 | const char* pass = "wireless password goes here"; 16 | 17 | // OAUTH2 Client credentials 18 | String client_id = "client id goes here"; 19 | String client_secret = "client secret goes here; 20 | 21 | // Tokens 22 | String access_token = "access token goes here"; 23 | String refresh_token = "refresh token goes here"; 24 | 25 | // Email Setup 26 | String email_from = "email account to log in to goes here"; 27 | String email_to = "email address to send an email to goes here"; 28 | 29 | // Sheets Setup 30 | String sheet_id = ""; 31 | String sheet_range = "Sheet1!A:B"; 32 | String status_code; 33 | /* ==================================== 34 | * Stop editing your configuration here 35 | */ 36 | 37 | // SSL Setup 38 | // http://askubuntu.com/questions/156620/how-to-verify-the-ssl-fingerprint-by-command-line-wget-curl/ 39 | // echo | openssl s_client -connect www.googleapis.com:443 | openssl x509 -fingerprint -noout 40 | const char* host = "www.googleapis.com"; 41 | const char* sheetsHost = "sheets.googleapis.com"; 42 | const int httpsPort = 443; 43 | const char* fingerprint = "put a fingerprint here using the command in the comment above"; 44 | 45 | // OAUTH2 Basics 46 | String access_type = "offline"; 47 | String redirect_uri = "urn:ietf:wg:oauth:2.0:oob"; 48 | String response_type = "code"; 49 | String auth_uri = "https://accounts.google.com/o/oauth2/auth"; 50 | String info_uri = "/oauth2/v3/tokeninfo"; 51 | String token_uri = "/oauth2/v4/token"; 52 | String authorization_code = ""; // leave empty 53 | 54 | // Send messages only. No read or modify privileges on mailbox. 55 | String gmail_scope = "https://www.googleapis.com/auth/gmail.send"; 56 | // Allows read/write access to the user's sheets and their properties. 57 | String sheet_scope = "https://www.googleapis.com/auth/spreadsheets"; 58 | // required to determine which user really authenticated 59 | String scope = "email " + gmail_scope + " " + sheet_scope; 60 | 61 | static const int ERROR_STATE = -1; 62 | // Moved INFO step to be first, so we check if the access token is valid before we try to use it. 63 | static const int INFO = 0; 64 | static const int INITIAL_STATE = 1; 65 | static const int AWAIT_CHALLANGE = 2; 66 | static const int EXCHANGING = 3; 67 | static const int REFRESHING = 4; 68 | static const int DO_IT = 5; 69 | static const int END_STATE = 6; 70 | 71 | // Set global variable attributes. 72 | static int CURRENT_STATE = INITIAL_STATE; 73 | 74 | 75 | bool appendToSheet() { 76 | 77 | String postData = ""; 78 | postData += "{\n \"values\": [[\"brasel\",\"fink\"]]\n}"; 79 | 80 | String postHeader = ""; 81 | postHeader += ("POST /v4/spreadsheets/" + sheet_id + "/values/" + sheet_range + ":append" + "?valueInputOption=raw" + " HTTP/1.1\r\n"); 82 | postHeader += ("Host: " + String(sheetsHost) + ":" + String(httpsPort) + "\r\n"); 83 | postHeader += ("Connection: close\r\n"); 84 | postHeader += ("Authorization: Bearer " + access_token + "\r\n"); 85 | postHeader += ("Content-Type: application/json; charset=UTF-8\r\n"); 86 | postHeader += ("Content-Length: "); 87 | postHeader += (postData.length()); 88 | postHeader += ("\r\n\r\n"); 89 | 90 | String result = postRequest(sheetsHost, postHeader, postData); 91 | 92 | return true; 93 | } 94 | 95 | bool getSheetContent() { 96 | // GET https://sheets.googleapis.com/v4/spreadsheets/spreadsheetId/values/Sheet1!A1:D5 97 | if (sheet_id != "" && sheet_range != "") { 98 | 99 | String reqHeader = ""; 100 | reqHeader += ("GET /v4/spreadsheets/" + sheet_id + "/values/" + sheet_range + "?access_token=" + access_token + " HTTP/1.1\r\n"); 101 | reqHeader += ("Host: " + String(sheetsHost) + ":" + String(httpsPort) + "\r\n"); 102 | reqHeader += ("Connection: close\r\n"); 103 | reqHeader += ("\r\n\r\n"); 104 | 105 | String result = getRequest(sheetsHost, reqHeader); 106 | return true; 107 | } else { 108 | return false; 109 | } 110 | } 111 | 112 | bool sendEmail(String body) { 113 | if (body != "") { 114 | String userId = "me"; 115 | String headers = ""; 116 | headers += "From: " + email_from + "\n"; 117 | headers += "To: " + email_to + "\n"; 118 | headers += "Subject: This is a test\n\n"; 119 | 120 | String email = base64::encode(headers + body); 121 | email.replace("\n", ""); 122 | 123 | String postData = ""; 124 | postData += "{\n \"raw\": \"" + email + "\"\n}"; 125 | 126 | String postHeader = ""; 127 | postHeader += ("POST /gmail/v1/users/" + userId + "/messages/send" + " HTTP/1.1\r\n"); 128 | postHeader += ("Host: " + String(host) + ":" + String(httpsPort) + "\r\n"); 129 | postHeader += ("Connection: close\r\n"); 130 | postHeader += ("Authorization: Bearer " + access_token + "\r\n"); 131 | postHeader += ("Content-Type: application/json; charset=UTF-8\r\n"); 132 | postHeader += ("Content-Length: "); 133 | postHeader += (postData.length()); 134 | postHeader += ("\r\n\r\n"); 135 | 136 | String result = postRequest(host, postHeader, postData); 137 | 138 | return true; 139 | } else { 140 | return false; 141 | } 142 | } 143 | 144 | String parseResponse(String response) { 145 | DynamicJsonBuffer jsonBuffer; 146 | JsonObject& root = jsonBuffer.parseObject(response); 147 | 148 | if (!root.success()) { 149 | Serial.println("parseObject() failed"); 150 | return ""; 151 | } else { 152 | String output; 153 | root.printTo(output); 154 | Serial.print("JSON: "); Serial.println(output); 155 | return output; 156 | } 157 | } 158 | 159 | // Removed requirement to only return if we get a 200 OK response to the GET request. The Google API responds with a 400 status code if the access token isn't valid. This way when the token is not valid, we can parse the response for the reason the request failed, instead of just getting nothing. 160 | String getRequest(const char* server, String request) { 161 | #ifdef DEBUG 162 | Serial.print("Function: "); Serial.println("getRequest()"); 163 | #endif 164 | 165 | String result = ""; 166 | 167 | // Use WiFiClientSecure class to create TLS connection 168 | WiFiClientSecure client; 169 | Serial.print("Connecting to: "); Serial.println(server); 170 | 171 | if (!client.connect(server, httpsPort)) { 172 | Serial.println("connection failed"); 173 | return result; 174 | } 175 | 176 | if (client.verify(fingerprint, server)) { 177 | #ifdef DEBUG 178 | Serial.println("certificate matches"); 179 | Serial.print("get: "); Serial.println(request); 180 | #endif 181 | 182 | client.print(request); 183 | 184 | #ifdef DEBUG 185 | Serial.println("request sent"); 186 | Serial.println("Receiving response"); 187 | #endif 188 | 189 | while (client.connected()) { 190 | if(client.find("HTTP/1.1 ")) { 191 | String status_code = client.readStringUntil('\r'); 192 | Serial.print("Status code: "); Serial.println(status_code); 193 | 194 | 195 | } 196 | if(client.find("\r\n\r\n")) { 197 | Serial.println("Data:"); 198 | } 199 | String line = client.readStringUntil('\r'); 200 | Serial.println(line); 201 | result += line; 202 | } 203 | 204 | Serial.println("closing connection"); 205 | return result; 206 | } else { 207 | Serial.println("certificate doesn't match"); 208 | } 209 | return result; 210 | } 211 | 212 | 213 | String postRequest(const char* server, String header, String data) { 214 | #ifdef DEBUG 215 | Serial.print("Function: "); Serial.println("postRequest()"); 216 | #endif 217 | 218 | String result = ""; 219 | 220 | // Use WiFiClientSecure class to create TLS connection 221 | WiFiClientSecure client; 222 | Serial.print("Connecting to: "); Serial.println(server); 223 | 224 | if (!client.connect(server, httpsPort)) { 225 | Serial.println("connection failed"); 226 | return result; 227 | } 228 | 229 | if (client.verify(fingerprint, server)) { 230 | #ifdef DEBUG 231 | Serial.println("certificate matches"); 232 | Serial.print("post: "); Serial.println(header + data); 233 | #endif 234 | 235 | client.print(header + data); 236 | 237 | #ifdef DEBUG 238 | Serial.println("request sent"); 239 | Serial.println("Receiving response"); 240 | #endif 241 | 242 | while (client.connected()) { 243 | if(client.find("HTTP/1.1 ")) { 244 | String status_code = client.readStringUntil('\r'); 245 | Serial.print("Status code: "); Serial.println(status_code); 246 | if(status_code != "200 OK") { 247 | Serial.println("There was an error"); 248 | break; 249 | } 250 | 251 | 252 | if(client.find("\r\n\r\n")) { 253 | Serial.println("Data:"); 254 | } 255 | String line = client.readStringUntil('\r'); 256 | Serial.println(line); 257 | result += line; 258 | 259 | 260 | } 261 | 262 | 263 | } 264 | 265 | 266 | Serial.println("closing connection"); 267 | return result; 268 | } else { 269 | Serial.println("certificate doesn't match"); 270 | } 271 | return result; 272 | } 273 | 274 | // create URL 275 | void authorize() { 276 | 277 | #ifdef DEBUG 278 | Serial.print("Function: "); Serial.println("authorize()"); 279 | #endif 280 | if (refresh_token == "") { 281 | String URL = auth_uri + "?"; 282 | URL += "scope=" + urlencode(scope); 283 | URL += "&redirect_uri=" + urlencode(redirect_uri); 284 | URL += "&response_type=" + urlencode(response_type); 285 | URL += "&client_id=" + urlencode(client_id); 286 | URL += "&access_type=" + urlencode(access_type); 287 | Serial.println("Goto URL: "); 288 | Serial.println(URL); Serial.println(); 289 | Serial.print("Enter code: "); 290 | CURRENT_STATE = AWAIT_CHALLANGE; 291 | } else { 292 | CURRENT_STATE = INFO; 293 | } 294 | } 295 | 296 | bool exchange() { 297 | #ifdef DEBUG 298 | Serial.print("Function: "); Serial.println("exchange()"); 299 | #endif 300 | 301 | if (authorization_code != "") { 302 | 303 | String postData = ""; 304 | postData += "code=" + authorization_code; 305 | postData += "&client_id=" + client_id; 306 | postData += "&client_secret=" + client_secret; 307 | postData += "&redirect_uri=" + redirect_uri; 308 | postData += "&grant_type=" + String("authorization_code"); 309 | 310 | String postHeader = ""; 311 | postHeader += ("POST " + token_uri + " HTTP/1.1\r\n"); 312 | postHeader += ("Host: " + String(host) + ":" + String(httpsPort) + "\r\n"); 313 | postHeader += ("Connection: close\r\n"); 314 | postHeader += ("Content-Type: application/x-www-form-urlencoded\r\n"); 315 | postHeader += ("Content-Length: "); 316 | postHeader += (postData.length()); 317 | postHeader += ("\r\n\r\n"); 318 | 319 | String result = postRequest(host, postHeader, postData); 320 | 321 | CURRENT_STATE = END_STATE; 322 | return true; 323 | } else { 324 | return false; 325 | } 326 | } 327 | // added JSON parser that checks if the parse is valid, extracts a new auth token from the response from the Google API, and sets it as the current auth token. 328 | bool refresh() { 329 | #ifdef DEBUG 330 | Serial.print("Function: "); Serial.println("refresh()"); 331 | #endif 332 | if (refresh_token != "") { 333 | 334 | String postData = ""; 335 | postData += "refresh_token=" + refresh_token; 336 | postData += "&client_id=" + client_id; 337 | postData += "&client_secret=" + client_secret; 338 | postData += "&grant_type=" + String("refresh_token"); 339 | 340 | String postHeader = ""; 341 | postHeader += ("POST " + token_uri + " HTTP/1.1\r\n"); 342 | postHeader += ("Host: " + String(host) + ":" + String(httpsPort) + "\r\n"); 343 | postHeader += ("Connection: close\r\n"); 344 | postHeader += ("Content-Type: application/x-www-form-urlencoded\r\n"); 345 | postHeader += ("Content-Length: "); 346 | postHeader += (postData.length()); 347 | postHeader += ("\r\n\r\n"); 348 | 349 | String result = postRequest(host, postHeader, postData); 350 | StaticJsonBuffer<300> jsonBuffer; 351 | JsonObject& root = jsonBuffer.parseObject(result); 352 | if (!root.success()) { 353 | Serial.println("parseObject() failed"); 354 | } 355 | else{ 356 | const char* access; 357 | access = root["access_token"]; 358 | access_token = String(access); 359 | Serial.println(access_token); 360 | } 361 | CURRENT_STATE = END_STATE; 362 | return true; 363 | } else { 364 | return false; 365 | } 366 | } 367 | // Added a JSON parser that checks if the parse is valid, extracts any error description, and begins a request for a new auth token via refresh(); if the current token is not valid. 368 | bool info() { 369 | #ifdef DEBUG 370 | Serial.print("Function: "); Serial.println("info()"); 371 | #endif 372 | if (access_token != "") { 373 | 374 | String reqHeader = ""; 375 | reqHeader += ("GET " + info_uri + "?access_token=" + access_token + " HTTP/1.1\r\n"); 376 | reqHeader += ("Host: " + String(host) + ":" + String(httpsPort) + "\r\n"); 377 | reqHeader += ("Connection: close\r\n"); 378 | reqHeader += ("\r\n\r\n"); 379 | Serial.println(host); 380 | Serial.println(reqHeader); 381 | String result = getRequest(host, reqHeader); 382 | 383 | // need to check for valid token here 384 | StaticJsonBuffer<200> jsonBuffer; 385 | JsonObject& root = jsonBuffer.parseObject(result); 386 | if (!root.success()) { 387 | Serial.println("parseObject() failed"); 388 | } 389 | else{ 390 | const char* validity; 391 | String validitystr; 392 | validity = root["error_description"]; 393 | validitystr = String(validity); 394 | if (validitystr == "Invalid Value"){ 395 | Serial.println("Invalid token, refreshing"); 396 | refresh();} 397 | } 398 | 399 | CURRENT_STATE = DO_IT; 400 | } else { 401 | CURRENT_STATE = REFRESHING; 402 | return false; 403 | } 404 | return true; 405 | } 406 | 407 | String urlencode(String str) 408 | { 409 | String encodedString = ""; 410 | char c; 411 | char code0; 412 | char code1; 413 | char code2; 414 | for (int i = 0; i < str.length(); i++) { 415 | c = str.charAt(i); 416 | if (c == ' ') { 417 | encodedString += '+'; 418 | } else if (isalnum(c)) { 419 | encodedString += c; 420 | } else { 421 | code1 = (c & 0xf) + '0'; 422 | if ((c & 0xf) > 9) { 423 | code1 = (c & 0xf) - 10 + 'A'; 424 | } 425 | c = (c >> 4) & 0xf; 426 | code0 = c + '0'; 427 | if (c > 9) { 428 | code0 = c - 10 + 'A'; 429 | } 430 | code2 = '\0'; 431 | encodedString += '%'; 432 | encodedString += code0; 433 | encodedString += code1; 434 | //encodedString+=code2; 435 | } 436 | yield(); 437 | } 438 | return encodedString; 439 | 440 | } 441 | 442 | // enable Serial communication 443 | String serialComm() { 444 | String result = ""; 445 | while (Serial.available()) { 446 | String inputString = Serial.readString(); 447 | inputString.trim(); 448 | if (inputString != "") { 449 | return inputString; 450 | } 451 | } 452 | return result; 453 | } 454 | 455 | void setup() { 456 | Serial.begin(115200); Serial.println(); 457 | 458 | Serial.print("Connecting to: "); Serial.println(ssid); 459 | WiFi.begin(ssid, pass); 460 | while (WiFi.status() != WL_CONNECTED) { 461 | delay(500); 462 | Serial.print("."); 463 | } 464 | 465 | Serial.println(); 466 | Serial.print("WiFi connected. "); Serial.print("IP address: "); Serial.println(WiFi.localIP()); 467 | Serial.println(); 468 | } 469 | 470 | void loop() { 471 | 472 | switch (CURRENT_STATE) { 473 | 474 | case INITIAL_STATE: 475 | authorize(); 476 | break; 477 | case AWAIT_CHALLANGE: 478 | authorization_code = serialComm(); 479 | if (authorization_code != "") { 480 | Serial.println("********"); 481 | CURRENT_STATE = EXCHANGING; 482 | } 483 | break; 484 | case EXCHANGING: 485 | exchange(); 486 | break; 487 | case INFO: 488 | info(); 489 | break; 490 | case REFRESHING: 491 | refresh(); 492 | break; 493 | case DO_IT: 494 | sendEmail("This test totally worked."); 495 | //appendToSheet(); 496 | //getSheetContent(); 497 | CURRENT_STATE = END_STATE; 498 | break; 499 | case END_STATE:{ 500 | 501 | break; 502 | default: 503 | Serial.println("ERROR"); 504 | break; 505 | } 506 | }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jan Almeroth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP8266-OAUTH2 2 | WIP🔥 Implementation of OAUTH2 for ESP8266 3 | 4 | Send emails using Gmail API and push/fetch data from Spreadsheets. More to come… 5 | --------------------------------------------------------------------------------