泰國Suwatchai K.先生寫了一個收發e-mail的Arduino程式庫,名叫“ESP Mail Client”(ESP郵件用戶端),支援ESP8266, ESP32以及採用SAMD21微控制器的Arduino開發板。本文將示範編寫一個從ESP32或ESP8266開發板傳送電子郵件的程式,在此之前,先認識一下寄信和收信的通訊協定。
電子郵件通訊協定:SMTP、POP3和IMAP
電子郵件並非直接寄送到收信者的手機或電腦裡面,而是寄到保管信件的郵件伺服器。郵件伺服器相當於郵局,信件會被轉送到收信人註冊帳號的那個郵件伺服器。
傳送和接收信件的通訊協定不一樣,我們不需要知道詳細的規範,但是必須認得它們的名字和用途。
- SMTP協定:Simple Mail Transfer Protocol,直譯為「簡單郵件傳送協定」,負責從用戶端傳送信件到郵件伺服器,或者在郵件伺服器之間轉送信件。
- POP3協定:Post Office Protocol version 3,直譯為「電子郵件協定第3版」,負責把信件從郵件伺服器下載到用戶端(如:電腦、手機)。
- IMAP協定:Internet Message Access Protocol,直譯為「網際網路訊息存取協定」,同樣用於接收郵件,跟POP3的主要不同點是,POP3必須把整個信件下載到用戶端才能閱讀,IMAP則允許用戶端直接瀏覽郵件伺服器上的信件。

“ESP Mail Client”支援SMTP和IMAP兩種協定,具備寄信、收信和檢視信件功能。
安裝ESP Mail Client程式庫
選擇Arduino IDE的「草稿碼→匯入程式庫→管理程式碼」,開啟「程式庫管理員」,在其中輸入關鍵字“esp mail client”,可找到兩個程式庫。請安裝上面那一個;另一個“ESP32 Mail Client”是不再維護的舊版本。

郵件伺服器的「應用程式密碼」
我們可以連接現有的郵件伺服器讓ESP32/ESP8266寄送信件,前提是要取得這些資料:
- 郵件伺服器的名稱
- 埠號
- 使用者帳號(也就是你的e-mail)
- 使用者密碼
假設你有Gmail信箱帳號,就可以透過Gmail的伺服器送信:
- SMTP伺服器名稱:smtp.gmail.com
SMTP埠號:587
若你有微軟的live.com或hotmail.com等的信箱帳號,可透過office365.com的伺服器送信:
- SMTP伺服器名稱:smtp.office365.com
SMTP埠號:587
在2022年5月30日之前,我們可以直接使用Gmail帳號和密碼,讓第三方應用程式(如:Arduino開發板)傳送信件,但現在不行了,必須先在Gmail網站替應用程式產生一個專屬密碼,而在產生「應用程式密碼」之前,你的Gmail信箱必須有啟用兩步驟驗證。
微軟的office365.com郵件伺服器也有類似的設定機制,底下是在Gmail的設定步驟示範,若要替office365.com郵件伺服器設定應用程式密碼,請點擊這個AppPassword連結。
產生Gmail的應用程式密碼
進入你的Gmail頁面,點擊右上角的使用者圖示→點擊「管理你的Google帳戶」。

點擊左側的「安全性」,再點擊右下方的「應用程式密碼」。

點擊「選取應用程式→其他(自訂名稱)」。

輸入自訂的應用程式名稱,這個名字不影響程式運作,可隨意設定。輸入名稱後,點擊「產生」。

Gmail將產生一個專屬密碼,請複製它,稍後需要將它輸入到Arduino程式。

透過ESP32/ESP8266開發板寄送電子郵件
前置作業準備就緒,回到Arduino IDE,底下是採用“ESP Mail Client”程式庫寄送e-mail的程式流程:

這是經由office365.com郵件伺服器寄信到Yahoo信箱的郵件內容。

底下是寄信的完整程式碼,改自 “ESP Mail Client” 的Send_HTML範例程式。
#if defined(ESP32)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#endif
#include <ESP_Mail_Client.h>
#define WIFI_SSID "你的Wi-Fi名稱"
#define WIFI_PASSWORD "Wi-Fi密碼"
// 採用Gmail的郵件伺服器
#define SMTP_HOST "smtp.gmail.com"
#define SMTP_PORT 465
#define AUTHOR_EMAIL "你的帳號@gmail.com"
#define AUTHOR_PASSWORD "上文產生的應用程式密碼"
#define SENDER_NAME "寄信人的名字"
#define MAIL_SUBJECT "信件的主旨"
#define RECIPIENT "收信人的名字"
#define RECIPIENT_EMAIL "收信人的e-amil"
/* 宣告用於寄信的SMTP Session物件 */
SMTPSession smtp;
/* 取得寄信狀態的回呼函式 */
void smtpCallback(SMTP_Status status);
void setup(){
Serial.begin(115200);
Serial.print("\n連接Wi-Fi");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED){
Serial.print(".");
delay(500);
}
Serial.println("\nWi-Fi已連線。");
Serial.println("IP位址:");
Serial.println(WiFi.localIP());
Serial.println();
/** 是否在序列埠監控視窗顯示除錯訊息
* 0代表不要
* 1代表顯示簡單除錯訊息
*/
smtp.debug(1);
/* 設定取得寄信狀態的回呼函式 */
smtp.callback(smtpCallback);
/* 宣告SMTP郵件伺服器連線物件 */
ESP_Mail_Session session;
/* 設定SMTP郵件伺服器連線物件 */
session.server.host_name = SMTP_HOST; // 設定寄信的郵件伺服器名稱
session.server.port = SMTP_PORT; // 郵件伺服器的埠號
session.login.email = AUTHOR_EMAIL; // 你的帳號
session.login.password = AUTHOR_PASSWORD; // 密碼
/* 設置時區 */
session.time.ntp_server = F("pool.ntp.org,time.nist.gov");
session.time.gmt_offset = 8; // 台北時區
session.time.day_light_offset = 0; // 無日光節約時間
/* 宣告SMTP訊息物件 */
SMTP_Message message;
/* 設定郵件標頭 */
message.sender.name = SENDER_NAME; // 寄信人的名字
message.sender.email = AUTHOR_EMAIL; // 寄信人的e-mail
message.subject = MAIL_SUBJECT; // 信件主旨
message.addRecipient(RECIPIENT, RECIPIENT_EMAIL); // "收信人的名字", "收信人的e-mail"
/* 設定郵件內容(HTML格式訊息) */
String htmlMsg = "<div style=\"color:#2f4468;\"><h1>學程式就是要動手實作</h1><p>- 從ESP32開發板傳送</p></div>";
message.html.content = htmlMsg.c_str(); // 設定信件內容
message.text.charSet = "utf-8"; // 設定訊息文字的編碼
/* 連線到郵件伺服器 */
if (!smtp.connect(&session))
return;
/* 開始寄信 */
if (!MailClient.sendMail(&smtp, &message))
Serial.println("寄信時出錯了:" + smtp.errorReason());
}
void loop(){
}
/* 取得信件寄送狀態的回呼函式本體 */
void smtpCallback(SMTP_Status status){
/* 顯示目前的狀態 */
Serial.println(status.info());
/* 顯示傳送結果 */
if (status.success()){
Serial.println("----------------");
ESP_MAIL_PRINTF("訊息傳送成功:%d\n", status.completedCount());
ESP_MAIL_PRINTF("訊息傳送失敗:%d\n", status.failedCount());
Serial.println("----------------\n");
struct tm dt;
for (int i = 0; i < smtp.sendingResult.size(); i++){
SMTP_Result result = smtp.sendingResult.getItem(i);
time_t ts = (time_t)result.timestamp;
ESP_MAIL_PRINTF("訊息編號:%d\n", i + 1);
ESP_MAIL_PRINTF("狀態:%s\n", result.completed ? "成功" : "失敗");
ESP_MAIL_PRINTF("日期/時間:%s\n", asctime(localtime(&ts)));
ESP_MAIL_PRINTF("收信人:%s\n", result.recipients.c_str());
ESP_MAIL_PRINTF("主旨:%s\n", result.subject.c_str());
}
Serial.println("----------------\n");
smtp.sendingResult.clear();
}
}
