2022年02月18日 | 公開。 |
2022年03月09日 | スケッチを、ESP8266だけではなくESP32でも動作する様に改良。それに従って、題名を「ESP8266を使ってGmail経由でメールを送信する」から「ESP8266/ESP32を使ってGmail経由でメールを送信する」に変更。 |
2024年09月27日 | Googleアカウントの仕様変更に応じて、内容を変更。 |
Wi-Fiに接続されているESP8266やESP32でGmailにログインし、メールを送信できたら便利だなと思い、その方法を調べました。ネット上にESP8266を使ってGmail経由でメールを送信する方法の解説記事がいくつかあるのですが、Gmailへのログイン方法は、セキュリティー上の理由から結構頻繁に変わっているらしく、現在のGmailにログインできる例は見つかりませんでした。そこで、ネット上の記事を参考に、自分で工夫してGmailにログインできるESP8266/ESP32のスケッチを作りましたので、紹介します。
なお、この記事ではメールの受信の仕方については扱いません。
この記事では、Arduino環境でESP8266やESP32の開発をする事を前提にお話しします。
ESP8266を使って、Gmail経由でメールを送信する方法については、Sahara's WebLogというブログで、次の様な2つの記事が公開されています。
上記2つの記事の内、1番目の記事は2016年に書かれたもので、当時としては正常に動作する、Gmail経由でのメール送信スケッチを紹介しているのですが、その後そのスケッチは、Gmail側の仕様変更で動作しなくなっています。
2番目の記事は2017年に書かれたもので、Gmailの仕様変更を受けて修正したスケッチを紹介しています。しかしこの修正後のスケッチも、Gmailのさらなる仕様変更により、この記事を書き始めた2022年3月現在では動作しなくなっています。
これらのブログ記事とは別に、OpenSSLを使ってGmail経由でメールを送信する方法が書いてあるQiitaの記事を見つけました。(下記リンク参照)
リンク先の記事では、SSL/TLSプロトコルで通信するOpenSSLを使って、SMTPサーバーに手動でメール送信コマンドを入力して、Gmail経由でメールを送信する方法を紹介しています。この記事で紹介されている方法で、実際にOpenSSLを使ってメールを送信してみると、メールがうまく送信できましたので、この送信手順をESP8266/ESP32のスケッチにすると、ESP8266/ESP32でメールが送信できるはずです。
Qiitaの記事を元に、Sahara's WebLogの記事も参考にしながら作ったスケッチを、リスト1に示します。
このスケッチは、ESP8266とESP32の共用のスケッチです。ESP8266用とESP32用でスケッチが異なる部分がわずかにありますが、#ifdef DSP8266
~#else
~#endif
コンパイラ指令を用いて、差のある部分は、ESP8266用とESP32用の両方のコードが書いてあります。Arduino IDEのボードの設定がESP8266とESP32のどちらになっているかに応じて、適切なコードを選択してコンパイルされるようになっています。
なお、このスケッチは、Arduino IDE 2.3.2とArduino core for ESP8266 WiFi chip 3.0.2(ESP8266の場合)あるいはArduino core for the ESP32, ESP32-S2 and ESP32-C3 3.0.5(ESP32の場合)の組み合わせで動作確認しています。
また、このスケッチは、この記事を書いている2024年9月現在正常に動作していますが、将来的にはGmailの仕様変更により動作しなくなる可能性があります。
/**
* @file gmail.ino
* @brief Gmail経由でメールを送信するスケッチ
*/
// Copyright (c) 2022 Hiroshi Tanigawa (Handle: Synapse)
// This sketch is distributed at http://synapse.kyoto
// This sketch is released under the MIT licence.
// https://opensource.org/licenses/mit-license.php
#define SECURE_MODE //!< SECURE_MODEを宣言すると、サーバー認証も行なう。コメントアウトすると、サーバー認証を省略する。プログラムは短くなるがセキュリティ的に甘くなる。
#ifdef ESP8266
#include <ESP8266WiFi.h>
#else
#include <WiFi.h>
#endif
#include <WiFiClientSecure.h>
const char SSID[]="yourSsid"; //!< Wi-FiのSSID。自分の環境に合わせて書き換える
const char PASSWORD[]="yourPassword"; //!< Wi-Fiのパスワード。自分の環境に合わせて書き換える
const char MAIL_ADDRESS[]="yourId@gmail.com"; //!< 送信元のGmailのメールアドレス
const char MAIL_PASSWORD[]="yourGmailPassword"; //!< 送信元のGmailのパスワード
const char TO_ADDRESS[]="sample@example.com"; //!< 送信先のメールアドレス
const char MAIL_SUBJECT[]="ESP8266/ESP32からのテストメール"; //!< メールの件名
//! メールの本文。改行する時には\r\nを挿入する。行が半角ピリオド(.)で始まる時は、半角ピリオドを二重(..)にする
const char MAIL_BODY[]="This mail was sent form an ESP8266/ESP32.\r\nESP8266/ESP32からこんにちは。";
// GmailのSMTPサーバーのURLとポート番号
const char SERVER_NAME[]="smtp.gmail.com"; //!< GmailのSMTPサーバーのURL
const int SERVER_PORT=465; //!< GmailのSMTPサーバーへの接続に使うポート番号
#ifdef SECURE_MODE
/**
* Google Trust Services LLCのルート証明書。有効期間が2036年6月22日で終わるので、その日より後では、
* SECURE_MODEをdefineしていると、このスケッチは動作しない。
*/
const char ROOT_CERT[]=
"-----BEGIN CERTIFICATE-----\n"
"MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH\n"
"MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM\n"
"QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy\n"
"MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl\n"
"cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB\n"
"AQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM\n"
"f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX\n"
"mX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7\n"
"zUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P\n"
"fyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc\n"
"vfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4\n"
"Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp\n"
"zBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO\n"
"Rc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW\n"
"k70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+\n"
"DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF\n"
"lQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\n"
"HQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW\n"
"Cu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1\n"
"d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z\n"
"XPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR\n"
"gyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3\n"
"d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv\n"
"J4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg\n"
"DdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM\n"
"+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy\n"
"F62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9\n"
"SQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws\n"
"E3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl\n"
"-----END CERTIFICATE-----\n";
#endif
WiFiClientSecure client; //!< SMTPサーバーとのTLS通信に使うクライアント
/**
* 文字列をBase64エンコードする。
* @param str エンコード前の文字列
* @returns エンコード後の文字列
*/
String base64(String str)
{
byte buf=0;
byte bufUsed=0;
const char convTbl[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
String result=F("");
for(int i=0; i<str.length(); i++) {
byte charCode=str[i];
buf=(buf<<(6-bufUsed))+(charCode>>(2+bufUsed));
result+=convTbl[buf];
charCode&=(1<<(2+bufUsed))-1;
if(bufUsed>=4) {
buf=charCode>>(bufUsed-4);
result+=convTbl[buf];
buf=charCode&&((1<<(bufUsed-4))-1);
bufUsed-=4;
} else {
bufUsed+=2;
buf=charCode;
} // if
} // for i
if(bufUsed>0) {
buf<<=(6-bufUsed);
result+=convTbl[buf];
} // if
int len=result.length();
if(len%4!=0) {
for(int i=0; i<4-len%4; i++) {
result+='=';
} // for i
} // if
return result;
} // base64
/**
* 文字列をシリアルとSMTPサーバーに送信する。
* @param str 送信したい文字列
*/
void sendString(String str)
{
Serial.print("Sending: ");
Serial.println(str);
client.print(str+"\r\n");
} // SendString
/**
* SMTPサーバーからの応答を待つ。10秒応答がなければタイムアウトする。
* サーバーの応答をシリアルに出力する。
* 応答コードが400番以上なら、エラーと判断して0を返す。
* @returns 400番未満の応答コードがSMTPサーバーから返れば1。400番未満の応答コードが返るかタイムアウトすれば0。
*/
byte eRcv()
{
byte respCode;
byte thisByte;
int loopCount = 0;
while (!client.available()) {
delay(1);
loopCount++;
// if nothing received for 10 seconds, timeout
if (loopCount>10000) {
client.stop();
Serial.println("\r\nTimeout");
return 0;
} // if
} // while
respCode = client.peek();
while (client.available()) {
thisByte = client.read();
Serial.write(thisByte);
} // while
if (respCode >= '4') {
return 0;
} // if
return 1;
} // eRcv
#ifdef SECURE_MODE
/**
* NTPで時刻を取得してGMTで内蔵時計を合わせる。時刻の取得は、サーバー認証の際に、証明書の
* 有効期限が終わっていないかを確認するために必要になる。
*/
void setClock() {
configTime(3*3600,0,"pool.ntp.org","time.nist.gov");
Serial.print("Waiting for NTP time sync");
time_t now=time(nullptr);
while (now<8*3600*2) {
delay(500);
Serial.print(".");
now = time(nullptr);
} // while
Serial.println("");
struct tm timeinfo;
gmtime_r(&now, &timeinfo);
Serial.print("Current time: ");
Serial.print(asctime(&timeinfo));
} // setClock
#endif
/**
* SMTPサーバーに接続し、メールを送信する。
*/
byte sendEmail()
{
byte thisByte=0;
byte respCode;
#ifdef SECURE_MODE
// NTPで内蔵時計の時刻を合わせる(証明書の期限の確認に、正確な時刻が必要)
setClock();
// ルート証明書を使ってサーバー認証を行なうモードに設定する
#ifdef ESP8266
X509List rootCert(ROOT_CERT);
client.setTrustAnchors(&rootCert);
#else // ifdef ESP8266
client.setCACert(ROOT_CERT);
#endif // ifdef ESP8266
#else // ifdef SECURE_MODE
// サーバー認証を行なわないモードに設定する(セキュリティ的には問題あり)
client.setInsecure();
#endif // ifdef SECURE_MODE
if (client.connect(SERVER_NAME,SERVER_PORT)==1) {
Serial.println(F("Connected to server."));
} else {
Serial.println(F("Connection failed."));
return 0;
} // if
if (!eRcv()) return 0;
sendString("EHLO www.example.com");
if (!eRcv()) return 0;
sendString("auth plain");
if (!eRcv()) return 0;
sendString(base64(String(MAIL_ADDRESS)+'\0'+MAIL_ADDRESS+'\0'+MAIL_PASSWORD));
if (!eRcv()) return 0;
sendString(String("MAIL From:<")+MAIL_ADDRESS+">");
if (!eRcv()) return 0;
sendString(String("RCPT To:<")+TO_ADDRESS+">");
if (!eRcv()) return 0;
sendString("DATA");
if (!eRcv()) return 0;
sendString(String("to:")+TO_ADDRESS);
sendString(String("from:")+MAIL_ADDRESS);
sendString(String("Subject:")+MAIL_SUBJECT);
sendString("");
sendString(MAIL_BODY);
sendString(".");
if (!eRcv()) return 0;
sendString("QUIT");
if (!eRcv()) return 0;
client.stop();
Serial.println("disconnected");
return 1;
} // sendEmail
/**
* setup関数。Wi-Fiに接続し、メールを送信する。
*/
void setup()
{
Serial.begin(115200);
delay(100);
Serial.println();
#ifdef SECURE_MODE
Serial.println("Running in secure mode.");
#else
Serial.println("Running in insecure mode.");
#endif
Serial.print("Connecting To ");
Serial.println(SSID);
WiFi.begin(SSID,PASSWORD);
while (WiFi.status()!=WL_CONNECTED) {
delay(500);
Serial.print(".");
} // while
Serial.println("");
Serial.println("WiFi Connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
byte ret=sendEmail();
} // setup
/**
* loop関数。何もしない。
*/
void loop()
{
} // loop
参考までに、Doxygenで作成した上記スケッチのドキュメントも用意しました。
リスト1のスケッチは、実行すると1回だけメールを送信します。
このスケッチを実行する前に、7つの定数を書き直す必要があります。
const char SSID[]="yourSsid"; //!< Wi-FiのSSID。自分の環境に合わせて書き換える
const char PASSWORD[]="yourPassword"; //!< Wi-Fiのパスワード。自分の環境に合わせて書き換える
const char MAIL_ADDRESS[]="yourId@gmail.com"; //!< 送信元のGmailのメールアドレス
const char MAIL_PASSWORD[]="yourGmailPassword"; //!< 送信元のGmailのパスワード
const char TO_ADDRESS[]="sample@example.com"; //!< 送信先のメールアドレス
const char MAIL_SUBJECT[]="ESP8266/ESP32からのテストメール"; //!< メールの件名
//! メールの本文。改行する時には\r\nを挿入する。行が半角ピリオド(.)で始まる時は、半角ピリオドを二重(..)にする
const char MAIL_BODY[]="This mail was sent form an ESP8266/ESP32.\r\nESP8266/ESP32からこんにちは。";
リスト2に、書き直しの必要な部分を抜き出しました。
SSIDは、使用するWi-FiのSSIDを表す文字列定数です。
PASSWORDは、使用するWi-Fiのパスワードを表す文字列定数です。
MAIL_ADDRESSは、メールの送信に使うGmailのメールアドレスを表す文字列定数です。
MAIL_PASSWORDは、Gmailにログインするのに必要なパスワードを表す文字列定数です。なお、このパスワードは、通常のパスワードではなく、アプリパスワードと呼ばれる、特殊なパスワードです。(後述)
参考:以前は、普通のパスワードでもログインできましたが、Gmailの仕様変更により、現在はアプリパスワードでしかログインできなくなっています。
TO_ADDRESSは、メールの送信先のアドレスを表す文字列定数です。
MAIL_SUBJECTは、メールの送信先の件名を表す文字列定数です。
MAIL_BODYは、メールの本文を表す文字列定数です。
メールの本文が複数の行になる場合は、改行部分に\r\nを挿入してください。例えば、本文を
こんにちは。 先日は楽しかったですね。 また会いましょう。
としたい場合は、MAIL_BODYの宣言を
const char MAIL_BODY[]="こんにちは。\r\n先日は楽しかったですね。\r\nまた会いましょう。";
としてください。
また、メールの本文中に、行頭が半角ピリオドで始まる行がある場合は、その半角ピリオドを二重にしてください。例えば、メールの本文を
.5mmのシャーペンの芯があった。
としたい場合は、MAIL_BODYの宣言を
const char MAIL_BODY[]="..5mmのシャーペンの芯があった。";
としてください。
参考:行頭の半角ピリオドを二重にするのは、SMTPの仕様です。SMTPでは、1つの半角ピリオドだけで終わる行は、メールの終わりを示す記号と解釈されるために、この様な仕様になりました。
これら7つの定数の宣言を書き直した上で、スケッチをコンパイルしてESP8266またはESP32に書き込み、実行すると、メールが送信されます。
次のページでは、定数MAIL_PASSWORDに設定する、アプリパスワードについて説明します。