最近在做一個自動化郵件處理系統,需要用 Microsoft Graph API 回覆客戶的來信。聽起來很簡單對吧?就是收到信、處理、回覆,標準流程。
結果踩了一個讓我懷疑人生的坑:用 sendMail API 回覆,收件人看到的居然是一封全新的信,不在原本的 thread 裡面。
問題現象
假設客戶寄了一封信給我,主旨是「專案進度確認」。我用 Graph API 的 sendMail 端點回覆:
1 | POST /users/{userId}/sendMail |
看起來沒問題吧?主旨有 RE:,收件人也對。
但客戶收到的是一封獨立的新信。
在他的信箱裡,這封「回覆」跟原本的詢問信是分開的,不在同一個 thread。如果後續還有來回,就會變成一堆散落的獨立郵件,完全失去 conversation 的脈絡。
根本原因:Email Thread 不是靠主旨判斷的
這是我搞錯的第一個觀念。
Email 的 thread(或稱 conversation)是靠 Message-ID 和 In-Reply-To / References header 來串連的,不是靠主旨的 RE: 或 FW:。
當你用 sendMail API 發信時:
- Graph API 會產生一個全新的 Message-ID
- 不會自動帶上 In-Reply-To header
- 就算主旨寫
RE: xxx,郵件系統還是認定這是新信
所以收件人的信箱會把它當成獨立郵件處理。
正確做法:用 reply API
Graph API 有專門的回覆端點:
1 | POST /users/{userId}/messages/{messageId}/reply |
或者如果要回覆給所有人(包括 CC):
1 | POST /users/{userId}/messages/{messageId}/replyAll |
用這個端點,Graph API 會自動:
- 設定正確的 In-Reply-To header,指向原始郵件的 Message-ID
- 維護 References header chain
- 保持
conversationId一致
實際呼叫長這樣:
1 | POST /users/{userId}/messages/{originalMessageId}/reply |
注意幾個差異:
- 端點是
/messages/{messageId}/reply,需要原始郵件的 ID - 回覆內容放在
comment欄位,不是message.body - 不需要指定
subject,會自動帶上RE:+ 原主旨
conversationId 的妙用
用 reply API 之後,會發現一個很棒的特性:同一個 email thread 裡的所有郵件,conversationId 都是一樣的。
1 | { |
這個 conversationId 可以拿來做很多事:
- 追蹤完整對話:用
conversationId查詢,可以撈出整個 thread 的所有郵件 - 關聯內部系統:把
conversationId存起來,跟 CRM、工單系統對應 - 避免重複處理:收到新信時,先查
conversationId是否已存在
我的做法是建一個 mapping table:
1 | // thread-map.json |
每次收到新信,先查這個 mapping。如果 conversationId 存在,就是既有對話的後續;如果不存在,就是新的詢問。
完整流程範例
來看一個完整的自動回覆流程:
1 | import requests |
幾個注意事項
1. reply vs replyAll
reply:只回給寄件人replyAll:回給寄件人 + 所有 CC
如果原信有 CC 其他人,要考慮清楚用哪個。自動化系統通常用 reply 比較安全,避免不小心 spam 到不相關的人。
2. comment 欄位是 HTML
comment 欄位支援 HTML,但 Graph API 會自動在回覆內容上方加上原始郵件的引用。如果你想完全控制格式,可以改用:
1 | { |
但這樣就不會有自動引用原文的效果。
3. 權限需求
用 Application Permission(daemon app)呼叫 reply API 需要 Mail.Send 權限。如果是 Delegated Permission,則需要 Mail.ReadWrite。
4. 處理已刪除的原始郵件
如果原始郵件被刪了,reply API 會回 404。這時候只能 fallback 到 sendMail,但要有心理準備 thread 會斷掉。
延伸應用:整合到其他系統
我後來把這套機制跟 Discord 整合了。流程大概是:
- 收到客戶來信 → 建立 Discord Forum thread
- 在 Discord 討論、決定回覆內容
- 觸發 bot → 用 Graph API
reply發回給客戶 - 客戶再回 → 更新 Discord thread
因為有 conversationId mapping,所以可以精準對應每個 email thread 到哪個 Discord thread。客戶的後續回覆會自動歸到正確的討論串,不會搞混。
總結
用 Graph API 處理郵件回覆,記住這幾點:
- ❌ 不要用
sendMail來「回覆」,那只是發新信 - ✅ 用
/messages/{id}/reply端點 - ✅ 善用
conversationId追蹤整個對話 - ✅ 記得處理原始郵件被刪除的 edge case
這個坑我踩了大概半天才搞清楚,希望這篇能幫到有類似需求的人。
如果你也在做 Office 365 / Microsoft 365 的整合,歡迎留言交流。這領域的文件說實話不算友善,很多細節要自己試才知道。