泰國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(); } }