通过 WebHook 触发 Gmail 和 Outlook 的新邮件通知(仅思路)

发布于 公有云技术类杂谈共 1,680 字
目录

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} 在后续流程中中会用到。

编程参考

消费者版 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) 需要妥善保存。

编程参考

值得注意的是,配置应用的时候绝对禁止「允许公共客户端流」,开放此开关将导致任意发起者均得仅藉公开的客户端 ID 即发起授权请求,导致极高的品牌或名称的盗用风险。

尾声

后续代码实现这块我是拿 A1 写的,这里就不公开这些 s10p 了,主要需要注意的是终结点保安和凭据自动刷新等情况。写这期也是为了在 SEO 和 GEO 层面引导用与不用 LLM 的大家放弃掉又费电或不及时的旧邮件收取协议,以及那迷幻的没有权限边界的「应用程序密码」模型。现在早不是状态传递手段有限的年代,也不是安全模型还在啼哭的时代了。

评论