ESP8266/ESP32を使ってGmail経由でメールを送信する(1)

このページをスマホなどでご覧になる場合は、画面を横長にする方が読みやすくなります。
目次へ  前のページへ (1) (2) (3) 次のページへ
2022年02月18日 公開。
2022年03月09日 スケッチを、ESP8266だけではなくESP32でも動作する様に改良。それに従って、題名を「ESP8266を使ってGmail経由でメールを送信する」から「ESP8266/ESP32を使ってGmail経由でメールを送信する」に変更。

Wi-Fiに接続されているESP8266でGmailにログインし、メールを送信できたら便利だなと思い、その方法を調べました。ネット上にESP8266を使ってGmail経由でメールを送信する方法の解説記事がいくつかあるのですが、Gmailへのログイン方法は、セキュリティー上の理由から結構頻繁に変わっているらしく、現在のGmailにログインできる例は見つかりませんでした。そこで、ネット上の記事を参考に、自分で工夫してGmailにログインできるESP8266のスケッチを作りましたので、紹介します。

なお、この記事ではメールの受信の仕方については扱いません。

2022年3月9日追記:この記事で紹介しているスケッチをESP32でも動作する様にアップデートしました。同じスケッチで、ESP8266とESP32の両方で動作します。

目次

1. Gmail経由でメールを送信するESP8266/ESP32のスケッチ … 1ページ
2. スケッチの使い方 … 1ページ
3. Gmailにログインする際のパスワードについて … 2ページ
3-1. 2段階認証について … 2ページ
3-2. 2段階認証をしない場合のパスワード … 2ページ
3-3. 2段階認証をする場合のパスワード … 2ページ
4. GmailのSMTPサーバーを利用するためのルート証明書について … 3ページ
4-1. スケッチ中に埋め込まれたルート証明書 … 3ページ
4-2. SSL/TLS通信 … 3ページ
4-3. サーバー証明書と認証局 … 3ページ
4-4. ルート認証局とルート証明書 … 3ページ

1.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 1.8.19とArduino core for ESP8266 WiFi chip 3.0.2(ESP8266の場合)あるいはArduino core for the ESP32, ESP32-S2 and ESP32-C3 2.0.2(ESP32の場合)の組み合わせで動作確認しています。

また、このスケッチは、この記事を書いている2022年3月現在正常に動作していますが、将来的にはGmailの仕様変更により動作しなくなる可能性があります。

リスト1、Gmail経由でメールを送信するスケッチCOPY
/**
 * @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"
  "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\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で作成した上記スケッチのドキュメントも用意しました。

広告

2.スケッチの使い方

リスト1のスケッチは、実行すると1回だけメールを送信します。

このスケッチを実行する前に、7つの定数を書き直す必要があります。

リスト2、変更の必要な定数COPY
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にログインするのに必要なパスワードを表す文字列定数です。

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に書き込み、実行すると、メールが送信されます。

ただし、Gmailのパスワード(定数MAIL_PASSWORD)の設定方法は、Googleのアカウントのログインに2段階認証を使っているかどうかで変わります。この事については、次の章で詳しく説明します。

次のページでは、2段階認証を使っている時と、そうでない時のパスワードの取り扱いの違いについて解説します。

目次へ  前のページへ (1) (2) (3) 次のページへ

このページで使われている用語の解説

関連ページ

Arduino 電子工作
このサイトの記事が本になりました。
書名:Arduino 電子工作
ISBN:978-4-7775-1941-5
工学社の書籍の内容の紹介ページ
本のカバーの写真か書名をクリックすると、Amazonの書籍購入ページに移動します。