iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
Build on AWS

從一個網站的誕生,看懂 AWS 架構與自動化的全流程!系列 第 22

Day 22 活動通知自動化:SES 打造客製化 Email 通知

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20251007/201727433KpXphJYlY.png

一、前言

在會員系統中,除了註冊與忘記密碼的郵件外,還會需要針對 促銷活動、系統公告、個人化提醒 發送通知。若用人工寄送,不僅效率低,也無法針對不同會員群組進行客製化。透過 Amazon SES (Simple Email Service),我們可以打造自動化的郵件通知系統,結合 Lambda 或 API Gateway,實現 即時 + 批次 + 個人化 的郵件寄送。

此 Lab 的定位是 行銷活動與系統事件通知模組,解決傳統 Email 發送的三大痛點:
(1) 大規模寄送困難:傳統 SMTP 伺服器可能被封鎖或限制。
(2) 缺乏個人化:無法針對使用者屬性(如名稱、活動偏好)調整內容。
(3) 寄送可靠性與監控不足:缺少投遞率與退信追蹤。

在 Serverless 架構中,它與 Cognito、API Gateway、DynamoDB 等服務結合,形成完整的會員通知管道。

二、需要使用到的服務

  1. Amazon SES:負責郵件發送、寄送統計、退信處理。
  2. AWS Lambda:用於產生動態 Email 內容,例如根據使用者資料生成個人化訊息。
  3. Amazon DynamoDB:儲存會員資料與偏好(如訂閱/退訂狀態、語系、活動紀錄)。
  4. Amazon EventBridge:定時或事件觸發,啟動郵件發送流程。
  5. API Gateway(可選):提供 API 介面給管理後台,讓管理員能觸發 Email 活動。

三、架構/概念圖

https://ithelp.ithome.com.tw/upload/images/20251006/20172743Rjhdt10nG7.png

四、技術重點

  1. 強化寄件信任度:務必設定 SPF、DKIM、DMARC,避免進垃圾郵件。
  2. 退訂機制:在郵件中加入退訂連結,確保合規(GDPR / CAN-SPAM)。
  3. 分群寄送:根據會員屬性(地區、語言、偏好)進行 Segmentation,提高點擊率。
  4. SES SandBox 限制:初期帳號在 SandBox 模式,每天限額 200 封郵件,需申請升級至 Production。
  5. 郵件範本管理:使用 SES Template 功能統一管理活動郵件樣式,避免硬編碼在 Lambda。

五、Lab流程

1️⃣ 前置作業

  1. 驗證SES寄件網域。(Day10)
  2. 建立同步Cognito會員資料的DynamoDB。(Day14)

2️⃣ 主要配置

1. 用CLI創建SES郵件範本(目前無法使用Console創建[1])

  1. 啟用AWS CLI,並創建一個JSON範本檔案。

    vim template.json
    

    https://ithelp.ithome.com.tw/upload/images/20251006/20172743QXE5xq0YxU.png

  2. 編輯「template.json」檔案內容。

    {
        "TemplateName": "Ducky_Campaign_Update_Template",
        "TemplateContent": {
            "Subject": "Hello {{name}}, 歡迎參加我們最新的 {{activity}} 活動!",
            "Text": "Dear {{name}},這是網頁活動測試信件.",
            "Html": "<h1>親愛的 {{name}},</h1><p>我們誠摯邀請您參加 {{activity}} 活動。這是一個透過 API 建立的範本!</p><p>活動詳情:<a href=\"{{link}}\">點此報名</a></p>"
        }
    }
    

    https://ithelp.ithome.com.tw/upload/images/20251006/20172743mQ8AUjaR9U.png

  3. 上傳本地端檔案「template.json」至SES範本內。

    • 範例程式碼

      aws sesv2 create-email-template --cli-input-json file://mytemplate.json
      

    https://ithelp.ithome.com.tw/upload/images/20251006/20172743OpVt4XyXGT.png

2. 確認SES範本已建立

  1. 進入「SES」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743YLba0uGwxi.png
  • 創建前:
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743AjqdaF3IAG.png

  • 創建後:
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743wh71fuZWZ0.png

3. 創建Lambda函數 - 寄送會員信件的模板

  1. 進入「Lambda」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743EK2GxYNfRk.png

  2. 創建一個新的函數。
    https://ithelp.ithome.com.tw/upload/images/20251006/201727439ik4GLa8vC.png

  3. 輸入函數名稱,並選擇編撰語言。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743u5696TBhkg.png

  4. 跳過建議畫面。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743wEBdFVUlf4.png

  5. 寫入程式碼,並部署。

    • 程式碼範例

      // index.mjs (SendCampaignEmail)
      import { SESClient, SendTemplatedEmailCommand } from "@aws-sdk/client-ses";
      import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
      import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
      
      // --- 服務初始化 ---
      const sesClient = new SESClient({ region: process.env.AWS_REGION });
      const ddbClient = new DynamoDBClient({});
      const ddbDocClient = DynamoDBDocumentClient.from(ddbClient);
      
      // --- 環境變數讀取 ---
      const SOURCE_EMAIL = process.env.SOURCE_EMAIL;
      const DDB_TABLE = process.env.DDB_TABLE;
      const TEMPLATE_NAME = process.env.TEMPLATE_NAME;
      
      // --- 活動內容定義 (可從 EventBridge 輸入中獲取,這裡固定寫死) ---
      const CAMPAIGN_CONTENT = {
          activity: "秋季資料分析研討會",
          link: "https://yourapp.com/events"
      };
      
      // 檢查環境變數是否設定
      if (!SOURCE_EMAIL || !DDB_TABLE || !TEMPLATE_NAME) {
          throw new Error('Required environment variables (SOURCE_EMAIL, DDB_TABLE, TEMPLATE_NAME) are missing.');
      }
      
      export const handler = async (event) => {
          let sentCount = 0;
      
          // 1. 從 DynamoDB 取得會員清單 (Scan)
          try {
              // 修正後的 Scan 邏輯 (無篩選,用於測試讀取所有資料)
      const scanParams = {
          TableName: DDB_TABLE,
          // 移除篩選條件,測試是否能讀取所有項目
          // FilterExpression 和 ExpressionAttributeValues 留空或移除
      };
      const scanResult = await ddbDocClient.send(new ScanCommand(scanParams));
      const users = scanResult.Items;
      
      // --- 關鍵修正:在程式碼中篩選並使用篩選結果 ---
      // 1. 篩選出 subscribed 屬性為布林值 true 的使用者清單
      const subscribedUsers = users.filter(user => user.subscribed === true); 
      
      // 輸出 Log 確認篩選結果
      console.log(`DEBUG: Total items scanned: ${users.length}`); 
      console.log(`DEBUG: Filtered subscribed users: ${subscribedUsers.length}`);
      
      // 接著,您的 for 迴圈應該對 subscribedUsers 進行迭代
      
              // 2. 逐一發送個人化 Email
              for (const user of subscribedUsers) {
                  const userName = user.name || '親愛的會員';
      
                  // 準備個人化數據 (TemplateData)
                  const templateData = {
                      name: userName, 
                      activity: CAMPAIGN_CONTENT.activity,
                      link: CAMPAIGN_CONTENT.link,
                      // 確保 email 欄位存在,因為 SES 需要
                      email: user.email 
                  };
      
                  const emailParams = {
                      Source: SOURCE_EMAIL,
                      Destination: { ToAddresses: [user.email] },
                      Template: TEMPLATE_NAME, 
                      // 將 JS 物件轉換為 JSON 字串,符合 SES API 要求
                      TemplateData: JSON.stringify(templateData) 
                  };
      
                  try {
                      const command = new SendTemplatedEmailCommand(emailParams);
                      await sesClient.send(command);
                      sentCount++;
                  } catch (e) {
                      console.error(`❌ Error sending email to ${user.email}:`, e);
                  }
              }
      
              console.log(`✅ Campaign finished. Total emails sent: ${sentCount}`);
      
              return { 
                  statusCode: 200, 
                  body: JSON.stringify({ 
                      message: 'Campaign finished successfully.', 
                      sentCount: sentCount 
                  }) 
              };
      
          } catch (dbError) {
              // 如果 DynamoDB 掃描失敗,直接回傳 500 錯誤
              console.error("❌ Fatal Error during DynamoDB scan:", dbError);
              return {
                  statusCode: 500,
                  body: JSON.stringify({ message: 'Failed to retrieve user list from database.' }),
              };
          }
      };
      

    https://ithelp.ithome.com.tw/upload/images/20251006/20172743A37UUrMvFq.png

  6. 進入「組態」分頁,設定環境變數。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743GZGkAzQYM2.png

  7. 設定以下環境變數
    SOURCE_EMAIL: no-reply@ducky-test.tw(寄件者信箱)
    DDB_TABLE: Ducky-userid(DynamoDB的表格名稱)
    TEMPLATE_NAME: Ducky_Campaign_Update_Template(SES範本名稱)
    https://ithelp.ithome.com.tw/upload/images/20251006/2017274328TY1RtIxR.png

    • DynamoDB的名稱哪裡找
      https://ithelp.ithome.com.tw/upload/images/20251006/20172743vzXKPkVxA7.png

    • TEMPLATE_NAME,SES範本名稱哪裡找?
      https://ithelp.ithome.com.tw/upload/images/20251006/201727430R92G2z0SH.png

  8. 完成畫面。
    https://ithelp.ithome.com.tw/upload/images/20251006/201727433OgN7QHZsf.png

4. 設定Lambda的IAM role

  1. 進入「IAM 」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743V0trtGK39y.png

  2. 進入IAM role的頁面,點選該Lambda自動創建的IAM role。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743gzyKyPcGBy.png

  3. 新增「許可政策」。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743KRzvlH3B10.png

  4. 授予AWS原本的基礎政策:AmazonSESFullAccess、AmazonDynamoDBReadOnlyAccess。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743nJS9yUX7s9.png

  5. 完成IAM role授權。
    https://ithelp.ithome.com.tw/upload/images/20251006/201727438S3fW0NF0H.png

5. 創建EventBridge - 定時觸發Lambda寄信

  • 建立規則:依照活動日期,發送活動通知。
  1. 進入「EventBridge」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743bnDYBHQnHD.png

  2. 創建規則。
    https://ithelp.ithome.com.tw/upload/images/20251006/201727437Fm79dQPN4.png

  3. 命名,並設定「排程」。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743QS75689Qal.png

  4. 此處可自定義時段。(這邊設定成每天晚上固定發信)
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743PfARkZOVoT.png

  5. 設定觸發動作(Lambda)。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743nYHDjrBAaO.png
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743vffJrBucJX.png

  6. 設定標籤,此處先跳過。(正式上線時,可以依照標籤「Label」來區隔費用)
    https://ithelp.ithome.com.tw/upload/images/20251006/2017274393wbhkznsi.png

  7. 完成建立。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743gp1OrPdima.png
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743aEWxyfzAeu.png

6. 在DynamoDB內新增「訂閱」欄位

  1. 進入DynamoDB內,編輯原有資料。
    https://ithelp.ithome.com.tw/upload/images/20251006/201727439nOA3qDrcf.png

  2. 增加欄位(布林值)「subscribed」。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743LEPWVcjyr0.png

  3. 完成畫面。
    https://ithelp.ithome.com.tw/upload/images/20251006/201727431GNORD0ylZ.png

3️⃣ 測試驗證

1. 新增一筆會員資料到 DynamoDB

💡記得先將該所有的收信E-mail,添加到SES,並完成驗證。
https://ithelp.ithome.com.tw/upload/images/20251006/20172743wwzWR17hFL.png

2. 執行 Lambda 測試發信

  1. 直接用範本做Lambda的發信測試。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743iZzsfHB06S.png

  2. 測試結果。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743o2QmnaXevg.png

3. 驗證投遞狀態

  1. 進入SES頁面,確認是否有發信成功。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743uOOCf6XinH.png

  2. 至信箱內確認是否有成功收到信件。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743xR75E7Lz0I.png
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743qjrrggiG7q.png

4. 測試訂閱/退訂:修改 subscribed=false,再次執行 Lambda,確認該會員不再收到郵件。

  1. 在DynamoDB中,編輯其中一個會員的資料。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743sFlRxKxmHc.png

  2. 取消其中一個user的訂閱,將「subscribed」改為False。
    https://ithelp.ithome.com.tw/upload/images/20251006/201727434kS5KS4TnT.png

  3. 完成調整。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743urWkSqT18d.png

  4. 進入Lambda頁面,重新做測試。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743HXqEHthWiR.png

  5. 測試結果頁面。(只剩下寄出一封信)
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743tE4apW3WZq.png

  6. 只會收到一封信。
    https://ithelp.ithome.com.tw/upload/images/20251006/20172743wKgFFo1oMQ.png

六、結語

今天的 Lab 讓我們完成了 會員活動通知自動化,透過 SES 與 Lambda 的組合,我們能根據會員屬性生成個人化郵件,並支援事件觸發與定時寄送。這樣的架構既能確保高可靠性(避免被封鎖),又能支援大規模行銷活動,真正做到 自動化 + 客製化 + 合規化 的郵件管理。


上一篇
Day 21 聰明檔案管理:S3 Lifecycle x Lambda 自動化版本控制與清理
下一篇
Day 23 管理員公告系統:API Gateway x SNS / SES 訊息發布
系列文
從一個網站的誕生,看懂 AWS 架構與自動化的全流程!24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言