NOTE
本文之自然语言均由人类输出。
以前的 Gmail Bot 经常掉会话,因此自建一个通知渠道十分有必要。去年的时候研究了一下 Zoho Mail 的 WebHook 通知,就在想是否可以运行一个类似方案,经过咨询 ChatGPT 研究出了办法——无非增加一个状态机,围绕状态机构建逻辑,听说还有安全模型授权。
本方案适用于全球云和主权云中的 Microsoft 365 Outlook, Google Workspace 以及消费者版 Gmail 和消费者版 Outlook。最终形态就是让用户向应用授予 mail read only 权限,让 Gmail 或 Outlook 向应用通知新邮件,应用拿着用户授予的权柄向 Gmail 或 Outlook 取件、格式化并将丰富的状态以各种形式通知到用户。
先决条件
与 Zoho Mail 能够直接向任意地址传出任意邮件内容不同,上述平台的 WebHook 均不能传出邮件内容,需要一个有状态的服务器程序向 Gmail API 或 Microsoft Graph 取得邮件内容,因此需要一个能够提供 WebHook 终结点的环境(虚拟机和云函数均可)。
对于消费者版 Gmail 或 Google Workspace 用途,需要一个 Google Cloud 项目;对于消费者版 Outlook 或全球云中的 Microsoft 365 Outlook 用途,需要一个 Microsoft Entra ID 租户;对于主权云中的 Microsoft 365 Outlook 用途,需要一个在相同主权云中的 Microsoft Entra ID 租户。如果没有 Google Cloud 项目、在全球云或相同主权云中的 Microsoft Entra ID 租户,请在开始之前创建一个。建议操作者账号拥有这些项目或租户的所有者或全局管理员权限。
消费者版 Gmail 或 Google Workspace 用途
设计架构为收件箱收到邮件事件触发 WebHook,程序受触发向 Gmail API 拉取新邮件并通知到 Bot。
启用相关 Google API
使用 Google Cloud Shell 执行以下内容。执行前,请将第 1 行中的 your-project-id 替换成自己的 Google Cloud 项目 ID:
PROJECT_ID="your-project-id"
gcloud services enable gmail.googleapis.com --project="$PROJECT_ID"
gcloud services enable pubsub.googleapis.com --project="$PROJECT_ID"
创建并配置 OAuth 2.0 客户端并配置
这部分截至发文并无 gcloud 操作办法,需要访问 Google Auth Platform: https://console.cloud.google.com/auth/clients/create?project=your-project-id 在访问前将 your-project-id 替换成自己的 Google Cloud 项目 ID.

「应用类型」选择「Web 应用」,「名称」可以自定义。

无需填写「已获授权的 JavaScript 来源」,在「已获授权的重定向 URI」中填写预计做回调的 URI 但不要填写 https://api.iks.moe/hooks/v1/google-auth-66ccff 单击「创建」即可。

「客户端密钥」系只显示一次之机密 (secret) 需要妥善保存。
创建并配置 Pub/Sub
使用 Google Cloud Shell 执行以下内容以为后续操作定义变量。执行前,请将第 1 行至第 5 行中的 your-project-id, your-topic-id, your-subscription-id, your-push-service-account-id 及 https://api.iks.moe/hooks/v1/google-push-66ccff 替换成自己的相应内容,并注意第 5 行的 PUSH_ENDPOINT 与上一段提到的「已获授权的重定向 URI」存在区别。
PROJECT_ID="your-project-id"
TOPIC_ID="your-topic-id"
SUBSCRIPTION_ID="your-subscription-id"
PUSH_SERVICE_ACCOUNT_ID="your-push-service-account-id"
PUSH_ENDPOINT="https://api.iks.moe/hooks/v1/google-push-66ccff"
PUSH_SERVICE_ACCOUNT="${PUSH_SERVICE_ACCOUNT_ID}@${PROJECT_ID}.iam.gserviceaccount.com"
PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format="value(projectNumber)")"
PUBSUB_SERVICE_AGENT="service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"
在同一个 Google Cloud Shell 会话继续执行以下内容:
gcloud pubsub topics create "$TOPIC_ID" --project="$PROJECT_ID"
将返回格式形如 projects/<PROJECT_ID>/topics/<TOPIC_ID> 的内容。继续在同一个 Google Cloud Shell 会话继续执行以下内容:
gcloud pubsub topics add-iam-policy-binding "$TOPIC_ID" \
--project="$PROJECT_ID" \
--member="serviceAccount:[email protected]" \
--role="roles/pubsub.publisher"
此处之 [email protected] 为系统服务账号,用于推送 Gmail 变动通知,不能更改。继续在同一个 Google Cloud Shell 会话继续执行以下内容,并将 your-service-account-display-name 替换为自己的相应内容:
gcloud iam service-accounts create "$PUSH_SERVICE_ACCOUNT_ID" \
--project="$PROJECT_ID" \
--display-name="your-service-account-display-name"
继续在同一个 Google Cloud Shell 会话继续执行以下内容:
gcloud iam service-accounts add-iam-policy-binding \
"$PUSH_SERVICE_ACCOUNT" \
--project="$PROJECT_ID" \
--member="serviceAccount:${PUBSUB_SERVICE_AGENT}" \
--role="roles/iam.serviceAccountTokenCreator"
继续在同一个 Google Cloud Shell 会话继续执行以下内容:
gcloud pubsub subscriptions create "$SUBSCRIPTION_ID" \
--project="$PROJECT_ID" \
--topic="projects/${PROJECT_ID}/topics/${TOPIC_ID}" \
--push-endpoint="$PUSH_ENDPOINT" \
--push-auth-service-account="$PUSH_SERVICE_ACCOUNT" \
--expiration-period="never"
形如 projects/${PROJECT_ID}/topics/${TOPIC_ID} 在后续流程中中会用到。
编程参考
- https://developers.google.com/workspace/gmail/api/guides/push
- https://developers.google.com/workspace/gmail/api/guides/sync
- https://docs.cloud.google.com/pubsub/docs/authenticate-push-subscriptions
- https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/get
消费者版 Outlook 或全球云和主权云中的 Microsoft 365 Outlook 用途
与 Gmail 的设计架构相同,全球云中的应用可以向 Microsoft Graph 全球终结点请求与 Microsoft 个人帐户关联的消费者数据和全球云中的数据,但主权云中的应用只能通过主权云终结点访问相应主权云中数据。
创建并配置应用注册
对于全球云中的 Azure,不具有有效 Azure 订阅的用户无权使用 Azure Cloud Shell 功能,需要访问 https://learn.microsoft.com/cli/azure/ 根据文档安装 Azure CLI 并使用 az login --allow-no-subscriptions 登录;具有有效 Azure 订阅的用户可以尝试在 Azure 门户中使用 Azure Cloud Shell 功能免于再次登录和可能的凭据泄露。对于主权云中的 Azure 无 Azure Cloud Shell 功能,需要在安装 Azure CLI 之后需要通过执行 az cloud set --name AzureChinaCloud 进行登录和后续流程(其中 AzureChinaCloud 可替换为 AzureUSGovernment, AzureGermanCloud, AzureBleuCloud 以及使用 Azure Stack 成立的混合云)。
执行以下内容以定义基本参数,将其中 your-tenant-id, your-app-display-name 和 https://api.iks.moe/hooks/v1/ms-aad-ra-66ccff 替换成自己的相应内容。对于 SIGN_IN_AUDIENCE 如「支持的帐户类型」为「仅限此组织目录中的帐户(单一租户)」则填写 AzureADMyOrg,为「任何组织目录中的帐户(任何 Microsoft Entra 目录 - 多租户)」则填写 AzureADMultipleOrgs,为「任何组织目录中的帐户(任何 Microsoft Entra 目录 - 多租户)和个人 Microsoft 帐户(例如 Skype、Xbox)」则填写 AzureADandPersonalMicrosoftAccount,为「仅 Microsoft 个人帐户」则填写 PersonalMicrosoftAccount;值得注意的是,主权云中的 Azure 只能为「仅限此组织目录中的帐户(单一租户)」和「任何组织目录中的帐户(任何 Microsoft Entra 目录 - 多租户)」进行应用注册。本文以通用性最强的 AzureADandPersonalMicrosoftAccount 为例。
TENANT_ID="your-tenant-id"
APP_DISPLAY_NAME="your-app-display-name"
REDIRECT_URI="https://api.iks.moe/hooks/v1/ms-aad-ra-66ccff"
SIGN_IN_AUDIENCE="AzureADandPersonalMicrosoftAccount"
登录前,需要选定活动租户,并获取租户 ID. 在同一个 Azure CLI 会话中执行以下内容以登录到选定的租户:
az login --tenant "$TENANT_ID" --allow-no-subscriptions
在同一个 Azure CLI 会话中执行以下内容:
APP_ID="$(
az ad app create \
--display-name "$APP_DISPLAY_NAME" \
--sign-in-audience "$SIGN_IN_AUDIENCE" \
--web-redirect-uris "$REDIRECT_URI" \
--enable-access-token-issuance false \
--enable-id-token-issuance false \
--is-fallback-public-client false \
--query "appId" \
--output tsv
)"
在同一个 Azure CLI 会话中执行以下内容:
az ad app permission add \
--id "$APP_ID" \
--api "00000003-0000-0000-c000-000000000000" \
--api-permissions "570282fd-fa5c-430d-a7fd-fc8dc98a9dca=Scope"
在同一个 Azure CLI 会话中执行以下内容,并将 client-secret-display-name 替换为自己的相应内容,并按需配置 years 的年限。
CLIENT_SECRET="$(
az ad app credential reset \
--id "$APP_ID" \
--append \
--display-name "client-secret-display-name" \
--years 2 \
--query "password" \
--output tsv
)"
printf 'Client ID: %s\n' "$APP_ID"
printf 'Client secret: %s\n' "$CLIENT_SECRET"
记住上述客户端 ID 和客户端秘密,后者系只显示一次之机密 (secret) 需要妥善保存。
编程参考
- https://learn.microsoft.com/entra/identity-platform/v2-oauth2-auth-code-flow
- https://learn.microsoft.com/graph/api/subscription-post-subscriptions
- https://learn.microsoft.com/graph/delta-query-messages
- https://learn.microsoft.com/graph/api/message-get
值得注意的是,配置应用的时候绝对禁止「允许公共客户端流」,开放此开关将导致任意发起者均得仅藉公开的客户端 ID 即发起授权请求,导致极高的品牌或名称的盗用风险。
尾声
后续代码实现这块我是拿 A1 写的,这里就不公开这些 s10p 了,主要需要注意的是终结点保安和凭据自动刷新等情况。写这期也是为了在 SEO 和 GEO 层面引导用与不用 LLM 的大家放弃掉又费电或不及时的旧邮件收取协议,以及那迷幻的没有权限边界的「应用程序密码」模型。现在早不是状态传递手段有限的年代,也不是安全模型还在啼哭的时代了。