Cloud Mail:基于 Cloudflare Workers、Resend 的自有域名邮箱服务搭建

2025 年 07 月 11 日
1152 字
6 分钟
AI 摘要
奋力赶来...

最近体验了新的开源项目 Cloud Mail,整合了 CloudflareResend,可以实现邮件的接收、邮件的发送,配置过程相对于 Gmail 的转发配置要更清晰一些。

为了简单操作,就直接使用 Github Action 的方式进行部署,完全不借助本地环境。

Cloudflare 配置过程

Cloudflare 会用到 D1、R2、KV、API Token、ACCOUNT_ID

创建 D1 数据库

UUID 类似 9fed5d9d-9c86-4311-aa27-940d79d2f67b,创建后可以直接点击复制。

创建 R2 存储桶

只需要用到创建的桶名称,并且绑定自定义域名。比如:email 和 https://r2.fylsen.com/

创建 KV

需要用到 ID,创建后直接复制,类似:fe314a95af404129a313dc9bf73b4b20

创建 API Token

通过“配置文件 - API 令牌”页面创建。

选择“编辑 Cloudflare Workers”模板进行创建,需要添加 D1 的编辑权限。

最终得到类似 KuqRuJWSDApZ8QA8IxHyWIb3twMVEhg4SsvdSOtJ 的 Token。

在 Cloudflare 操作页面中找到自己的用户ID

Github 配置过程

Github 中需要配置 Secret 并调整一下 Github Action。

设置 Secrets

访问 Cloud Mail,点击“Fork”。

在自己 Fork 后仓库中的 Settings 中添加之前获得的参数,需要根据自己的操作调整下面的具体值。

toml
ADMIN=admin@example.com(管理员邮箱)
CLOUDFLARE_ACCOUNT_ID=****
CLOUDFLARE_API_TOKEN=KuqRuJWSDApZ8QA8IxHyWIb3twMVEhg4SsvdSOtJ
D1_DATABASE_ID=9fed5d9d-9c86-4311-aa27-940d79d2f67b
DOMAIN=['fylsen.com','example.com']
JWT_SECRET=123123(让 GPT 生成随机字符串)
KV_NAMESPACE_ID=fe314a95af404129a313dc9bf73b4b20
R2_BUCKET_NAME=email

修改 Github Action(可能)

项目中.github/workflows/deploy-cloudflare.yml使用的逻辑会直接采用密钥方式保存环境变量,但是“domain”需要采用 JSON 格式进行保存,所以做了修改。

yaml
name: 🚀 Deploy cloud-mail to Cloudflare Workers

on:
  push:
    branches: [main]
    paths:
      - 'mail-worker/**'
      - 'mail-vue/**'
  workflow_dispatch:

jobs:
  Deploy-cloud-mail:
    name: 🏗️ Build and Deploy
    runs-on: ubuntu-latest

    env:
      CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
      CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
      D1_DATABASE_ID: ${{ secrets.D1_DATABASE_ID }}
      KV_NAMESPACE_ID: ${{ secrets.KV_NAMESPACE_ID }}
      R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
      DOMAIN: ${{ secrets.DOMAIN }}
      ADMIN: ${{ secrets.ADMIN }}
      JWT_SECRET: ${{ secrets.JWT_SECRET }}
      INIT_URL: ${{ secrets.INIT_URL }}

    outputs:
      deployment_skipped: ${{ steps.deploy.outputs.deployment_skipped }}

    steps:
      - name: ➡️ Checkout repository
        uses: actions/checkout@v4

      - name: 📦 Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: './mail-worker/package-lock.json'

      - name: 📥 Install dependencies
        run: npm ci
        working-directory: ./mail-worker

      - name: 📡 Disable wrangler telemetry
        working-directory: ./mail-worker
        run: npx wrangler telemetry disable -c wrangler-action.toml

      - name: 🤫 Set Worker secrets (ignore if already exists)
        working-directory: ./mail-worker
        run: |
          WORKER_NAME="cloud-mail"
          CONFIG_FILE="wrangler-action.toml"
          echo "🔒 Attempting to create/update secrets using '$CONFIG_FILE'."

          for VAR in ADMIN JWT_SECRET; do
            if [ -n "${!VAR}" ]; then
              VAR_LOWER=$(echo "$VAR" | tr '[:upper:]' '[:lower:]')
              echo ">> Processing secret: '$VAR_LOWER'"
              (echo "${!VAR}" | npx wrangler secret put "$VAR_LOWER" --name "$WORKER_NAME" -c "$CONFIG_FILE") || true
            else
              echo "⚠️ Warning: GitHub Secret '$VAR' is not set. Skipping."
            fi
          done

          echo "✨ Secret processing complete."

      - name: 🛠️ Prepare Config and Deploy
        id: deploy
        working-directory: ./mail-worker
        run: |
          if [ -z "$D1_DATABASE_ID" ] || [ -z "$KV_NAMESPACE_ID" ] || [ -z "$R2_BUCKET_NAME" ]; then
            echo "⚠️ Required secrets (D1_DATABASE_ID, KV_NAMESPACE_ID, or R2_BUCKET_NAME) are not set."
            echo "🟡 Skipping deployment."
            echo "deployment_skipped=true" >> $GITHUB_OUTPUT
            exit 0
          fi

          echo "deployment_skipped=false" >> $GITHUB_OUTPUT
          CONFIG_FILE="wrangler-action.toml"
          echo "⚙️ Dynamically updating '$CONFIG_FILE' with binding IDs..."

          sed -i "s|\${D1_DATABASE_ID}|${D1_DATABASE_ID}|g" "$CONFIG_FILE"
          sed -i "s|\${KV_NAMESPACE_ID}|${KV_NAMESPACE_ID}|g" "$CONFIG_FILE"
          sed -i "s|\${R2_BUCKET_NAME}|${R2_BUCKET_NAME}|g" "$CONFIG_FILE"

          if [ -n "$DOMAIN" ]; then
            if echo "$DOMAIN" | jq . > /dev/null 2>&1; then
              ESCAPED_DOMAIN=$(echo "$DOMAIN" | sed 's/"/\\"/g' | tr -d '\n')
              sed -i "s|\${DOMAIN}|${ESCAPED_DOMAIN}|g" "$CONFIG_FILE"
            else
              sed -i "s|\${DOMAIN}|${DOMAIN}|g" "$CONFIG_FILE"
            fi
          else
            sed -i "s|\${DOMAIN}||g" "$CONFIG_FILE"
          fi

          echo "🚀 Configuration updated. Starting deployment..."
          npx wrangler deploy -c "$CONFIG_FILE" | grep -v "https://.*\.workers\.dev" || true
          echo "✅ Deployment command executed."

      - name: 🗄️ Initialize Database (if INIT_URL is set)
        if: steps.deploy.outputs.deployment_skipped == 'false'
        run: |
          if [ -z "$INIT_URL" ]; then
            echo "✅ Deployment successful. INIT_URL not set, skipping initialization."
            exit 0
          fi

          echo "⏳ Waiting 10 秒之前 before checking initialization status..."
          sleep 10

          HTTP_CODE=$(curl -s -w "%{http_code}" -o response.txt "$INIT_URL")
          RESPONSE_BODY=$(cat response.txt)

          echo "🔎 Checking response... (Status: $HTTP_CODE)"

          if [ "$HTTP_CODE" = "200" ] && [ "$RESPONSE_BODY" = "初始化成功" ]; then
            echo "🎉✅ Fresh initialization successful!"
          elif [ "$HTTP_CODE" = "200" ]; then
            echo "✅ Database is already initialized or in a stable state. Response: $RESPONSE_BODY"
          else
            echo "⚠️ Database initialization check failed with HTTP status: $HTTP_CODE. Please check your worker logs."
          fi

      - name: 📣 Notify Final Status
        if: always()
        run: |
          if [ "${{ job.status }}" == "success" ]; then
            if [ "${{ steps.deploy.outputs.deployment_skipped }}" == "true" ]; then
              echo "🟡 Deployment was skipped due to missing configuration."
            else
              echo "🎉🎉🎉 Hooray! Deployment completed successfully! 🎉🎉🎉"
            fi
          else
            echo "❌❌❌ Oh no! The deployment failed. Please check the logs above for errors. ❌❌❌"
          fi

      - name: Delete workflow runs
        uses: GitRML/delete-workflow-runs@main
        with:
          retain_days: '3'
          keep_minimum_runs: '0'

Resend 设置

绑定自己的域名

需要根据提示,在 Cloudflare 中完成 DNS 的设置。

创建 API Key

设置 Webhooks

根据自己的域名设置,比如:https://email.example.com/api/webhooks,勾选email.bounced、email.complained、email.delivered、email.delivery_delayed。

部署

执行 Github Actions

Workers 绑定域名

邮寄转发到worker

在Cloudflare→账户主页→example.com→电子邮件→电子邮件路由→路由规则→Catch-all地址,编辑发送到worker。

项目初始化

访问https://email.example.com/api/init/{{生成的jwt_secret}}完成数据库的初始化。

配置

使用自己设置的管理员邮箱(admin@example.com)注册并登录,在系统设置中添加 Resend API Key 和 R2 的访问域名

剩余配置可以根据需要进行调整。

最后

我目前只使用发件功能,收件功能还是直接转发到常用的邮箱中。发件更容易,添加一个全新的域名邮箱成本基本为零,省去了单个配置的麻烦。

缺点依然是使用 Resend 服务的老问题,收件人邮箱依然会显示代发。

祝各位玩的开心。

文章标题:Cloud Mail:基于 Cloudflare Workers、Resend 的自有域名邮箱服务搭建

文章作者:Xuesong

文章链接:https://fylsen.com/posts/2025/07/cloudflare-workers-resend-custom-domain-email-service[复制]

最后修改时间:


商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文遵循 CC BY-NC-SA 4.0 许可协议。