真實踩坑: Thomas 設了一個每天早晚自動發摘要郵件的定時任務,裡面用了 Gmail App Password + SMTP 發郵件。腳本本身沒問題(本地測試登入成功),但定時任務跑了好幾個月,從來沒收到過一封。最後查出原因:雲端環境封鎖了 SMTP 465 端口,所有發送請求靜默失敗。解決方法是改用 Gmail REST API(走 HTTPS 443),雲端不封這個。
🔀 三種 Claude 收發 Gmail 的方式
Gmail MCP
透過 claude.ai 的 OAuth 連接器,讓 Claude 直接操作 Gmail。設定一次,永久有效。
只讀 + 草稿,不能直接發送
Google API Token
用你在 GCP 申請的 OAuth 全家桶 Token,透過 Gmail REST API 直接發送郵件。走 HTTPS,雲端也能用。
✅ 推薦:讀 + 寫 + 發送全功能
App Password + SMTP
用 Gmail 的應用程式密碼透過 SMTP 465 端口發送郵件。本地可用,但雲端被封。
❌ 雲端環境 SMTP 端口被封
📊 三種方式功能對比
| 功能 | Gmail MCP | Google API Token | App Password SMTP |
|---|---|---|---|
| 搜索郵件 | ✅ | ✅ | ❌ |
| 讀取郵件內容 | ✅ | ✅ | ❌ |
| 建立草稿 | ✅ | ✅ | ❌ |
| 直接發送郵件 | ❌ | ✅ | ✅ |
| 帶附件發送 | ❌ | ✅ | ✅ |
| 雲端環境可用 | ✅ | ✅ | ❌(SMTP被封) |
| 操作 Drive / YouTube | ❌ | ✅ | ❌ |
| 設定難度 | 最簡單 | 中等 | 簡單 |
| Token 可撤銷 | ✅ | ✅ | 需手動刪 |
🖥️ 三個使用環境能調用什麼
💡 核心限制
Google API Token 存在 GCP Secret Manager,需要 gcloud CLI 才能讀取。只有本地電腦裝了 gcloud,手機 APP 和雲端任務都碰不到它。
🖥️ 電腦 Claude Code
✅ Google API Token(有gcloud)
✅ Gmail MCP
✅ App Password SMTP
✅ 本地文件 / Bash
📱 手機 Claude APP
❌ Google API Token(無gcloud)
✅ Gmail MCP(只讀+草稿)
❌ App Password SMTP
❌ 本地文件
☁️ RemoteTrigger 雲端
⚠️ Token 直接內嵌可用
✅ Gmail MCP(只讀+草稿)
❌ SMTP(端口被封)
✅ HTTPS REST API(443)
🚫 為什麼雲端封 SMTP?
背景知識: AWS、GCP、Azure 等主流雲端平台默認封鎖 TCP 端口 25、465、587(SMTP 端口)。原因是防止被濫用發垃圾郵件。這個封鎖是平台層面的,不是你的代碼問題,也不是密碼問題。
解決方法: 改用 Gmail REST API,走 HTTPS 443 端口。這個端口所有雲端平台都開放,因為它就是普通的 HTTPS 網頁請求。
解決方法: 改用 Gmail REST API,走 HTTPS 443 端口。這個端口所有雲端平台都開放,因為它就是普通的 HTTPS 網頁請求。
| 發送方式 | 端口 | 雲端環境 | 原因 |
|---|---|---|---|
| SMTP (App Password) | 465 / 587 | ❌ 被封 | 防垃圾郵件策略 |
| Gmail REST API | 443 (HTTPS) | ✅ 可用 | 標準 HTTPS 請求 |
| MCP (OAuth) | 443 (HTTPS) | ⚠️ 可讀不可發 | MCP 工具集限制 |
📤 用 Gmail REST API 發送郵件(Python)
這是雲端任務裡用的完整代碼,不依賴任何外部庫,只用 Python 標準庫:
import urllib.request, urllib.parse, json, base64, datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# OAuth 憑證(直接內嵌,雲端無法用 gcloud)
CLIENT_ID = 'your-client-id.apps.googleusercontent.com'
CLIENT_SECRET = 'your-client-secret'
REFRESH_TOKEN = 'your-refresh-token'
# Step 1: 用 refresh_token 換取 access_token
tok_data = urllib.parse.urlencode({
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'refresh_token': REFRESH_TOKEN,
'grant_type': 'refresh_token'
}).encode()
with urllib.request.urlopen(
urllib.request.Request('https://oauth2.googleapis.com/token',
data=tok_data, method='POST')
) as r:
access_token = json.loads(r.read())['access_token']
# Step 2: 組裝郵件
msg = MIMEMultipart('alternative')
msg['Subject'] = '測試郵件'
msg['From'] = 'thomastangnz@gmail.com'
msg['To'] = 'thomastangnz@gmail.com'
msg.attach(MIMEText('Hello!
', 'html', 'utf-8'))
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
# Step 3: 用 Gmail API 發送
with urllib.request.urlopen(urllib.request.Request(
'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
data=json.dumps({'raw': raw}).encode(),
headers={'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'},
method='POST'
)) as r:
result = json.loads(r.read())
print('Sent! ID:', result.get('id'))
🔑 關鍵:refresh_token 和 access_token 的區別
- refresh_token:長期有效,不會過期(除非撤銷授權),存在 GCP Secret Manager 或直接內嵌
- access_token:短期有效(約1小時),每次執行都要用 refresh_token 換一個新的
- 代碼裡每次都重新換 token,不需要存儲 access_token
⏰ 怎麼升級雲端定時任務
診斷問題:本地測 App Password 登入成功 → 確認不是密碼問題。
結論:是雲端環境封了 SMTP 465 端口,靜默失敗。
結論:是雲端環境封了 SMTP 465 端口,靜默失敗。
取得 OAuth 憑證:從 GCP Secret Manager 讀取 refresh_token、client_id、client_secret。
禁用舊觸發器:用 RemoteTrigger update {enabled: false} 禁用舊任務(API 不支持刪除,去 claude.ai/code/scheduled 手動刪)。
創建新觸發器:用 RemoteTrigger create,腳本裡將 SMTP 改為 Gmail REST API。OAuth 憑證直接內嵌在腳本裡(雲端沒有 gcloud)。
測試:用 RemoteTrigger run 立即觸發一次,5分鐘後檢查收件箱。
⚠️ 安全提醒
雲端觸發器腳本裡的 refresh_token 是明文的。任何能訪問你 claude.ai 帳號的人都能看到。
相比 App Password:refresh_token 可以隨時在 Google 帳號授權頁面 撤銷,而且撤銷後馬上失效。App Password 被盜後需要手動找到並刪除,相對麻煩。
實際建議:帳號開啟 2FA,定期檢查 Google 帳號的已授權應用列表。
相比 App Password:refresh_token 可以隨時在 Google 帳號授權頁面 撤銷,而且撤銷後馬上失效。App Password 被盜後需要手動找到並刪除,相對麻煩。
實際建議:帳號開啟 2FA,定期檢查 Google 帳號的已授權應用列表。