精通 ESP32 SMTP | 使用 GMail 發送郵件


ESP32 SMTP 整合 Gmail 寄信功能是 IoT 專案中不可或缺的實用技能之一。本篇完整教學將涵蓋 Wi-Fi 連線、TLS 加密連線 Gmail、SMTP 命令交握、Base64 認證等技術細節,適合希望進一步強化 ESP32 應用能力的開發者。

想知道 ESP32 如何透過 SMTP 傳送 Gmail 郵件?這篇教學將帶你一步步從連接 Wi-Fi 到成功發送第一封郵件,完整解析 Gmail SMTP 認證、TLS 安全連線與寄件流程!

ESP32 SMTP

什麼是 SMTP?

SMTP(Simple Mail Transfer Protocol,簡單郵件傳輸協定)是一種用來傳送電子郵件的網際網路標準協定。簡單來說,它就是電郵的「郵差」,負責把你的郵件從發件人這端,傳遞到收件人的郵件伺服器。

ESP32 是一款支援 Wi-Fi 的微控制器,我們就可以使用 ESP32 SMTP 搭配 Gmail 或其他郵件服務,讓 ESP32 自動寄出 Email

開發環境

在開始編程之前,請確保已完成以下準備工作:

配置 Wi-Fi 連接

ESP32 需要連接至一個 Wi-Fi 網路才能存取網際網路,並連線至 Gmail 的 SMTP 伺服器。

// WiFi credentials - replace with your actual network information
#define WIFI_SSID "your_wifi_ssid"
#define WIFI_PASS "your_wifi_password"

// Function to initialize and connect to WiFi in station mode
void wifi_init_sta() {
    // Create default WiFi station network interface
    esp_netif_create_default_wifi_sta();

    // Initialize WiFi with default configuration
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // Configure WiFi station settings
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,          // Set SSID (network name)
            .password = WIFI_PASS,       // Set WiFi password
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,  // Minimum security protocol
        },
    };

    // Set WiFi to station mode (client mode)
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    
    // Apply the WiFi configuration
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
    
    // Start the WiFi service
    ESP_ERROR_CHECK(esp_wifi_start());
    
    // Initiate connection to the configured WiFi network
    ESP_ERROR_CHECK(esp_wifi_connect());
}

Wi-Fi 事件處理

我們需要監聽 Wi-Fi 狀態變化,特別是獲得 IP 位址後啟動發送郵件的任務。

static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT)
    {
        switch (event_id)
        {
        case WIFI_EVENT_STA_START:
            ESP_LOGI(TAG, "WiFi station started");
            break;
        case WIFI_EVENT_STA_CONNECTED:
            ESP_LOGI(TAG, "WiFi connected successfully");
            break;
        case WIFI_EVENT_STA_DISCONNECTED:
            ESP_LOGI(TAG, "WiFi disconnected");
            break;
        }
    }
    else if (event_base == IP_EVENT)
    {
        if (event_id == IP_EVENT_STA_GOT_IP)
        {
            ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
            ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));

            // Create SMTP task once we have an IP address
            xTaskCreate(smtp_task, "smtp_task", SMTP_TASK_STACK_SIZE, NULL, 5, NULL);
        }
    }
}

建立 ESP32 SMTP 連接

我們將使用 Gmail 的 SMTP 伺服器進行郵件發送。Gmail 的 SMTP 伺服器地址為 smtp.gmail.com,端口為 465,使用 SSL/TLS 協議進行加密。

在建立連接時,ESP32 SMTP 需要支持 SSL/TLS,因此我們需要使用 esp_tls 庫。

Base64 編碼函數

ESP32 SMTP 認證過程中需要對郵箱和密碼進行 Base64 編碼。我們寫了簡單的 base64_encode 函數來完成這個操作。

char* base64_encode(const unsigned char *data, size_t input_length) {
    const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    size_t output_length = 4 * ((input_length + 2) / 3);
    char *encoded_data = malloc(output_length + 1);
    
    if (encoded_data == NULL) return NULL;

    for (size_t i = 0, j = 0; i < input_length;) {
        uint32_t octet_a = i < input_length ? data[i++] : 0;
        uint32_t octet_b = i < input_length ? data[i++] : 0;
        uint32_t octet_c = i < input_length ? data[i++] : 0;
        uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;

        encoded_data[j++] = base64_table[(triple >> 3 * 6) & 0x3F];
        encoded_data[j++] = base64_table[(triple >> 2 * 6) & 0x3F];
        encoded_data[j++] = base64_table[(triple >> 1 * 6) & 0x3F];
        encoded_data[j++] = base64_table[(triple >> 0 * 6) & 0x3F];
    }

    for (size_t i = 0; i < (3 - input_length % 3) % 3; i++) {
        encoded_data[output_length - 1 - i] = '=';
    }
    encoded_data[output_length] = '\0';
    return encoded_data;
}

發送郵件函數

在郵件發送過程中,我們使用了 SSL/TLS 加密與 Gmail SMTP 伺服器進行通信,並透過 ESP32 SMTP 協議發送郵件。

void send_email() {
    ESP_LOGI(TAG, "Starting SMTP connection, free heap: %d bytes", esp_get_free_heap_size());

    // Configure TLS settings for secure SMTP connection
    esp_tls_cfg_t tls_cfg = {
        .timeout_ms = SMTP_TIMEOUT * 1000,  // Connection timeout
        .crt_bundle_attach = esp_crt_bundle_attach,  // Use ESP32's certificate bundle
    };

    // Initialize TLS structure
    esp_tls_t *tls = esp_tls_init();
    if (!tls) {
        ESP_LOGE(TAG, "Failed to initialize TLS");
        return;
    }

    // Establish SSL connection to SMTP server (port 465 for SMTPS)
    if (!esp_tls_conn_new_sync(SMTP_HOST, strlen(SMTP_HOST), SMTP_PORT, &tls_cfg, tls)) {
        ESP_LOGE(TAG, "SSL connection failed");
        esp_tls_conn_delete(tls);
        return;
    }

    // SMTP protocol exchange
    if (!smtp_exchange(tls, NULL, "220")) goto cleanup;  // Wait for server greeting
    if (!smtp_exchange(tls, "EHLO ESP32\r\n", "250")) goto cleanup;  // Send EHLO
    
    // Authentication process
    char *auth_cmd = malloc(128);
    if (!auth_cmd) goto cleanup;
    
    // Initiate LOGIN authentication
    if (!smtp_exchange(tls, "AUTH LOGIN\r\n", "334")) goto free_auth;
    
    // Send base64-encoded username
    char *encoded = base64_encode((const unsigned char *)SENDER_EMAIL, strlen(SENDER_EMAIL));
    if (!encoded) goto free_auth;
    snprintf(auth_cmd, 128, "%s\r\n", encoded);
    free(encoded);
    
    if (!smtp_exchange(tls, auth_cmd, "334")) goto free_auth;
    
    // Send base64-encoded password
    encoded = base64_encode((const unsigned char *)SENDER_PASSWORD, strlen(SENDER_PASSWORD));
    if (!encoded) goto free_auth;
    snprintf(auth_cmd, 128, "%s\r\n", encoded);
    free(encoded);
    
    if (!smtp_exchange(tls, auth_cmd, "235")) goto free_auth;  // Expect authentication success
    
    // Prepare email content
    char *email_data = malloc(256);
    if (!email_data) goto free_auth;
    
    // Set sender
    snprintf(email_data, 256, "MAIL FROM:<%s>\r\n", SENDER_EMAIL);
    if (!smtp_exchange(tls, email_data, "250")) goto free_all;
    
    // Set recipient
    snprintf(email_data, 256, "RCPT TO:<%s>\r\n", RECIPIENT_EMAIL);
    if (!smtp_exchange(tls, email_data, "250")) goto free_all;
    
    // Begin data transmission
    if (!smtp_exchange(tls, "DATA\r\n", "354")) goto free_all;
    
    // Compose email headers and body
    snprintf(email_data, 256,
        "From: %s\r\nTo: %s\r\nSubject: ESP32 Test\r\n\r\n"
        "Hello World!\r\n.\r\n",  // The dot on a line by itself ends the message
        SENDER_EMAIL, RECIPIENT_EMAIL);
    
    // Send the actual email content
    smtp_exchange(tls, email_data, "250");  // Expect 250 OK response
    
free_all:
    free(email_data);
free_auth:
    free(auth_cmd);
cleanup:
    // Gracefully terminate the SMTP session
    smtp_exchange(tls, "QUIT\r\n", NULL);
    esp_tls_conn_delete(tls);
    ESP_LOGI(TAG, "SMTP transaction completed");
}

Google 應用密碼

由於 Google 對於直接使用帳戶密碼進行第三方應用登入做了安全限制,尤其是在啟用了「兩步驟驗證」(2FA)功能的帳戶中,您必須使用 Google 應用密碼 來進行此類操作。這是一種專門為單一應用或設備生成的密碼,可以替代帳戶密碼進行身份驗證。

步驟 1:啟用 Google 兩步驗證

  1. 登入到 Google 帳戶
  2. 啟用兩步驗證
    • 在「安全性」部分,啟用「兩步驗證」。
    • 按照提示完成兩步驗證的設置,您需要綁定手機或其他驗證方式。

步驟 2:生成應用密碼

  1. 生成應用密碼
    • 在啟用兩步驗證後,返回到 Google 帳戶的「安全性」部分,找到 「應用密碼」
    • 點擊 「生成應用密碼」 按鈕。
    • 選擇「應用」並選擇「其他(自定義名稱)」,然後輸入例如「ESP32 SMTP」作為名稱。
    • 點擊生成,Google 會顯示一個 16 位的應用密碼(例如:abcd efgh ijkl mnop)。
  2. 複製應用密碼
    • 複製生成的應用密碼,這就是您在 ESP32 程式碼中需要使用的密碼。

步驟 3:在程式碼中使用應用密碼

在程式碼中,您將不再使用 Google 帳戶的登入密碼,而是使用剛剛生成的 應用密碼。在下面的程式碼範例中,您需要替換為您的應用密碼,而不是原本的 Google 帳戶密碼。

#define SENDER_PASSWORD "your_app_password"  // Use generated app-specific password for Gmail

完整的 ESP32 SMTP 程式碼

最終的 ESP32 SMTP 完整程式碼如下:

#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_tls.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "esp_crt_bundle.h"

// Configuration Section ============================================
#define WIFI_SSID "your_wifi_ssid"        // Wi-Fi SSID
#define WIFI_PASS "your_wifi_password"    // Wi-Fi password
#define SMTP_HOST "smtp.gmail.com"        // Gmail SMTP server address
#define SMTP_PORT 465                     // SMTP port (SSL)
#define SENDER_EMAIL "your_email@gmail.com"  // Sender's Gmail address
#define SENDER_PASSWORD "your_app_password"  // Gmail app password (generated in Google Account)
#define RECIPIENT_EMAIL "recipient_email@gmail.com"  // Recipient's email address
#define SMTP_TIMEOUT 10                   // SMTP timeout in seconds
#define SMTP_TASK_STACK_SIZE 8192        // Stack size for SMTP task
#define MAX_RETRIES 5                    // Maximum retries for sending email
// ===============================================================

static const char *TAG = "SMTP_CLIENT";
static int s_retry_num = 0;

// Base64 encoding function
char* base64_encode(const unsigned char *data, size_t input_length) {
    const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    size_t output_length = 4 * ((input_length + 2) / 3);
    char *encoded_data = malloc(output_length + 1);
    
    if (encoded_data == NULL) return NULL;

    for (size_t i = 0, j = 0; i < input_length;) {
        uint32_t octet_a = i < input_length ? data[i++] : 0;
        uint32_t octet_b = i < input_length ? data[i++] : 0;
        uint32_t octet_c = i < input_length ? data[i++] : 0;
        uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;

        encoded_data[j++] = base64_table[(triple >> 3 * 6) & 0x3F];
        encoded_data[j++] = base64_table[(triple >> 2 * 6) & 0x3F];
        encoded_data[j++] = base64_table[(triple >> 1 * 6) & 0x3F];
        encoded_data[j++] = base64_table[(triple >> 0 * 6) & 0x3F];
    }

    for (size_t i = 0; i < (3 - input_length % 3) % 3; i++) {
        encoded_data[output_length - 1 - i] = '=';
    }
    encoded_data[output_length] = '\0';
    return encoded_data;
}

// Function to handle SMTP communication with the server
bool smtp_exchange(esp_tls_t *tls, const char *send_str, const char *expect_resp) {
    char *buf = malloc(256);  // Allocate buffer for response
    if (!buf) {
        ESP_LOGE(TAG, "Failed to allocate buffer");
        return false;
    }

    if (send_str != NULL) {
        ESP_LOGI(TAG, "C: %s", send_str);  // Log sent command
        if (esp_tls_conn_write(tls, send_str, strlen(send_str)) < 0) {
            free(buf);
            return false;
        }
    }

    int len = esp_tls_conn_read(tls, buf, 255);
    if (len <= 0) {
        free(buf);
        return false;
    }
    buf[len] = '\0';
    ESP_LOGI(TAG, "S: %.128s", buf);  // Log server response (limited to 128 chars)

    bool success = (expect_resp == NULL) || (strstr(buf, expect_resp) != NULL);
    free(buf);
    return success;
}

// Function to send email
void send_email() {
    ESP_LOGI(TAG, "Starting SMTP connection, remaining heap: %d", esp_get_free_heap_size());

    // Establish SSL connection
    esp_tls_cfg_t tls_cfg = {
        .timeout_ms = SMTP_TIMEOUT * 1000,
        .crt_bundle_attach = esp_crt_bundle_attach,
    };

    esp_tls_t *tls = esp_tls_init();
    if (!tls) {
        ESP_LOGE(TAG, "Failed to initialize TLS");
        return;
    }

    // Connect using SSL (port 465)
    if (!esp_tls_conn_new_sync(SMTP_HOST, strlen(SMTP_HOST), SMTP_PORT, &tls_cfg, tls)) {
        ESP_LOGE(TAG, "SSL connection failed");
        esp_tls_conn_delete(tls);
        return;
    }

    // SMTP protocol exchange
    if (!smtp_exchange(tls, NULL, "220")) goto cleanup;
    if (!smtp_exchange(tls, "EHLO ESP32\r\n", "250")) goto cleanup;
    
    // Authentication process
    char *auth_cmd = malloc(128);
    if (!auth_cmd) goto cleanup;
    
    if (!smtp_exchange(tls, "AUTH LOGIN\r\n", "334")) goto free_auth;
    
    char *encoded = base64_encode((const unsigned char *)SENDER_EMAIL, strlen(SENDER_EMAIL));
    if (!encoded) goto free_auth;
    snprintf(auth_cmd, 128, "%s\r\n", encoded);
    free(encoded);
    
    if (!smtp_exchange(tls, auth_cmd, "334")) goto free_auth;
    
    encoded = base64_encode((const unsigned char *)SENDER_PASSWORD, strlen(SENDER_PASSWORD));
    if (!encoded) goto free_auth;
    snprintf(auth_cmd, 128, "%s\r\n", encoded);
    free(encoded);
    
    if (!smtp_exchange(tls, auth_cmd, "235")) goto free_auth;
    
    // Email content
    char *email_data = malloc(256);
    if (!email_data) goto free_auth;
    
    snprintf(email_data, 256, "MAIL FROM:<%s>\r\n", SENDER_EMAIL);
    if (!smtp_exchange(tls, email_data, "250")) goto free_all;
    
    snprintf(email_data, 256, "RCPT TO:<%s>\r\n", RECIPIENT_EMAIL);
    if (!smtp_exchange(tls, email_data, "250")) goto free_all;
    
    if (!smtp_exchange(tls, "DATA\r\n", "354")) goto free_all;
    
    snprintf(email_data, 256,
        "From: %s\r\nTo: %s\r\nSubject: ESP32 Test\r\n\r\n"
        "Hello World!\r\n.\r\n",
        SENDER_EMAIL, RECIPIENT_EMAIL);
    
    smtp_exchange(tls, email_data, "250");
    
free_all:
    free(email_data);
free_auth:
    free(auth_cmd);
cleanup:
    smtp_exchange(tls, "QUIT\r\n", NULL);
    esp_tls_conn_delete(tls);
    ESP_LOGI(TAG, "SMTP process completed");
}

// SMTP task function
void smtp_task(void *pvParameters) {
    ESP_LOGI(TAG, "SMTP task started");

    // Send email
    send_email();

    vTaskDelete(NULL);  // Task ends
}

// Wi-Fi event handler function
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT)
    {
        switch (event_id)
        {
        case WIFI_EVENT_STA_START:
            ESP_LOGI(TAG, "WiFi STA started");
            break;
        case WIFI_EVENT_STA_CONNECTED:
            ESP_LOGI(TAG, "WiFi connected successfully");
            s_retry_num = 0;
            break;
        case WIFI_EVENT_STA_DISCONNECTED:
            if (s_retry_num < MAX_RETRIES)
            {
                ESP_LOGI(TAG, "Attempting to reconnect (%d/%d)", s_retry_num + 1, MAX_RETRIES);
                esp_wifi_connect();
                s_retry_num++;
            }
            else
            {
                ESP_LOGE(TAG, "Connection failed, exceeded maximum retry attempts");
            }
            break;
        }
    }
    else if (event_base == IP_EVENT)
    {
        if (event_id == IP_EVENT_STA_GOT_IP)
        {
            ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
            ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));

            // Create SMTP task
            xTaskCreate(smtp_task, "smtp_task", SMTP_TASK_STACK_SIZE, NULL, 5, NULL);
        }
    }
}

// Wi-Fi initialization
void wifi_init_sta() {
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // Register event handler
    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());
    ESP_ERROR_CHECK(esp_wifi_connect());
}

void app_main() {
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    
    wifi_init_sta();

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

編譯和燒錄

完成程式碼後,您可以使用 ESP-IDF 提供的命令進行編譯、燒錄和監控。

檢查你的收件匣

在燒錄並執行程式後,請檢查你的郵件信箱,確認是否已收到 ESP32 裝置發送的測試郵件

結論

透過本專案的完整實作,我們全面掌握了在 ESP32 平台上整合 SMTP 郵件服務的核心技術。這個 ESP32 SMTP 解決方案充分發揮了晶片的Wi-Fi和硬體加密加速優勢,採用TLS 安全協定建立與 ESP32 SMTP 伺服器的加密連線。在實作過程中,我們特別針對 ESP32 SMTP 協定堆疊進行了記憶體管理優化,透過分塊處理技術有效解決了資源受限環境下發送大型郵件的難題,同時保持了完整的 SMTP 協定相容性。