返回博客
OTP验证 自动化 CI/CD DevOps 邮件API

在 CI/CD 流水线中自动化提取邮件 OTP 验证码

5 分钟阅读

如果你的应用通过邮件发送 OTP 验证码——用于注册、密码重置或双因素认证——你很可能已经遇到过这样的困境:自动化测试套件运行到一半突然卡住,等待一个真人去打开收件箱、复制 6 位数字、再粘贴到表单里。这一个手动步骤,彻底打破了全自动 CI/CD 流水线的承诺。

Google Authenticator 等 TOTP 认证器解决的是另一个问题:它们生成基于时间的动态码,用于应用层 2FA,对拦截发送到真实邮箱的一次性验证码毫无帮助。短信 OTP 就更麻烦了——你需要实体 SIM 卡、运营商 API 或专用硬件。邮件 OTP 则恰好处于一个甜蜜点:只要有合适的 API,就能可靠地自动化。

本文将使用 GridInbox,通过 JavaScript/Node.js、Python 和 GitHub Actions YAML 的真实可运行代码,手把手演示完整的实现方案。

一、邮件 OTP 提取的工作原理

从邮件中以编程方式获取 OTP 验证码,有两种架构思路:

API 轮询(Polling)

测试代码定期调用收件箱 API,检查是否有新邮件到达。如果含有 OTP 的邮件还没来,就等待一段时间后再试——最多重试若干次。这是 CI/CD 场景中最简单的方式,因为它只需要从 Runner 发起出站 HTTP 请求,不需要暴露任何公网端点。

Webhook 推送(Push)

邮件服务在新邮件到达的瞬间主动通知你的端点。速度更快,大规模场景下效率更高,但需要测试基础设施具备一个可公网访问的 HTTPS 端点——在许多 CI 环境中(一次性容器、防火墙内的 Runner 等)这并不现实。

对于绝大多数 CI/CD 场景,轮询是更优选择。2 秒的轮询间隔配合 20 秒的超时,既足够快,又不会消耗太多 API 配额。

OTP 解析的挑战

OTP 验证码在邮件中以各种不同的格式出现:

  • 纯数字:847291
  • 连字符分隔:847-291
  • HTML 加粗:<strong>847291</strong>
  • 出现在主题行:"您的验证码是 847291"
  • 被空白或标点符号包围在段落中

为几十个不同发件方编写鲁棒的正则表达式,既繁琐又容易出错。GridInbox 在服务端使用多种匹配策略进行提取——你的代码只需从 JSON 响应中读取一个干净的 otp 字段即可。

二、GridInbox API 响应示例

查询收件箱最新邮件时,GridInbox 返回结构化的 JSON 响应。只要服务器在邮件中检测到数字验证码,otp 字段就会被自动填充:

{
  "id": "msg_abc123",
  "from": "[email protected]",
  "subject": "您的验证码是 847291",
  "otp": "847291",              // 服务端自动解析,无需自己写正则
  "received_at": "2026-04-01T10:23:45Z",
  "text_body": "您的验证码是 847291,10 分钟内有效。",
  "html_body": "<p>您的验证码是 <strong>847291</strong></p>"
}

你无需编写任何正则。如果邮件中没有可识别的 OTP,otp 字段为 null——轻松过滤到你真正需要的那封邮件。text_bodyhtml_body 也一并提供,方便你对邮件内容进行额外断言。

最佳实践:为每个测试环境使用专属收件箱(如 [email protected][email protected]),防止不同环境的 OTP 相互干扰。

三、完整 JavaScript / Node.js 示例

以下辅助函数会轮询 GridInbox 收件箱,直到出现包含 OTP 的邮件,或达到最大重试次数为止。将它放入你的测试工具库,在任何需要 OTP 提取的地方引入即可:

// otp-helper.js
const API_BASE = 'https://api.gridinbox.com/api/v1';
const API_KEY = process.env.GRIDINBOX_API_KEY; // 从环境变量读取,切勿硬编码

/**
 * 轮询收件箱,等待 OTP 验证码出现
 * @param {string} mailboxId - GridInbox 收件箱 ID
 * @param {object} options - maxAttempts: 最大重试次数, delayMs: 每次间隔毫秒数
 * @returns {Promise<string>} OTP 字符串
 */
async function waitForOtp(mailboxId, { maxAttempts = 10, delayMs = 2000 } = {}) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    console.log(`轮询第 ${attempt}/${maxAttempts} 次...`);

    const res = await fetch(`${API_BASE}/mailboxes/${mailboxId}/messages?limit=1`, {
      headers: { 'X-API-Key': API_KEY }
    });

    if (!res.ok) throw new Error(`API 错误: ${res.status}`);

    const { messages } = await res.json();

    // 检查最新一封邮件是否包含 OTP
    if (messages && messages.length > 0 && messages[0].otp) {
      console.log(`OTP 已获取: ${messages[0].otp}`);
      return messages[0].otp;
    }

    // 未获取到则等待后重试
    if (attempt < maxAttempts) {
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }

  throw new Error(`已重试 ${maxAttempts} 次,未能获取到 OTP`);
}

module.exports = { waitForOtp };

在 Playwright 或 Puppeteer 测试中的使用示例:

// registration.spec.js
const { waitForOtp } = require('./otp-helper');

test('用户可以完成 OTP 验证流程', async ({ page }) => {
  // 触发发送 OTP 邮件(例如提交注册表单)
  await page.fill('#email', '[email protected]');
  await page.click('#send-otp');

  // 从收件箱 API 等待并获取 OTP
  const otp = await waitForOtp('mbx_your_mailbox_id', { maxAttempts: 15, delayMs: 2000 });

  // 填入表单并提交
  await page.fill('#otp-input', otp);
  await page.click('#verify');

  // 断言验证成功
  await expect(page.locator('#success-message')).toBeVisible();
});

该测试在 CI 中完全无头(headless)运行,零人工介入。waitForOtp 调用通常在 2–6 秒内返回,远在正常测试超时预算之内。

四、Python 示例

对于基于 pytest、Selenium 或其他 Python 自动化框架的项目,使用 requests 库实现等效的辅助函数:

# otp_helper.py
import time
import requests
import os

API_BASE = 'https://api.gridinbox.com/api/v1'
API_KEY = os.environ['GRIDINBOX_API_KEY']  # 从环境变量读取

def wait_for_otp(mailbox_id, max_attempts=10, delay_seconds=2):
    """
    轮询收件箱,直到获取到 OTP 验证码或超过最大重试次数。

    参数:
        mailbox_id: GridInbox 收件箱 ID
        max_attempts: 最大轮询次数(默认 10)
        delay_seconds: 每次轮询间隔秒数(默认 2)
    返回:
        str: OTP 字符串
    """
    headers = {'X-API-Key': API_KEY}

    for attempt in range(1, max_attempts + 1):
        print(f'轮询第 {attempt}/{max_attempts} 次...')

        response = requests.get(
            f'{API_BASE}/mailboxes/{mailbox_id}/messages',
            headers=headers,
            params={'limit': 1}  # 只取最新一封
        )
        response.raise_for_status()

        data = response.json()
        messages = data.get('messages', [])

        # 检查是否已有包含 OTP 的邮件
        if messages and messages[0].get('otp'):
            otp = messages[0]['otp']
            print(f'OTP 已获取: {otp}')
            return otp

        # 未获取到则等待后重试
        if attempt < max_attempts:
            time.sleep(delay_seconds)

    raise TimeoutError(f'已重试 {max_attempts} 次,未能获取到 OTP')

对应的 pytest 测试用例:

# test_registration.py
from selenium.webdriver.common.by import By
from otp_helper import wait_for_otp

def test_otp_verification(driver):
    """测试用户完整的 OTP 验证注册流程"""
    driver.get('https://yourapp.com/register')

    # 填写邮箱并触发 OTP 发送
    driver.find_element(By.ID, 'email').send_keys('[email protected]')
    driver.find_element(By.ID, 'send-otp').click()

    # 从 GridInbox 收件箱 API 获取 OTP
    otp = wait_for_otp('mbx_your_mailbox_id', max_attempts=15, delay_seconds=2)

    # 填入验证码并提交
    driver.find_element(By.ID, 'otp-input').send_keys(otp)
    driver.find_element(By.ID, 'verify').click()

    # 断言注册成功
    assert '欢迎' in driver.page_source or 'Welcome' in driver.page_source

五、集成到 GitHub Actions

将 GridInbox API Key 存储为仓库 Secret(命名为 GRIDINBOX_API_KEY),然后在工作流中引用。以下是一个针对 Node.js + Playwright 端到端测试的完整示例:

# .github/workflows/e2e.yml
name: 端到端测试(含 OTP 自动化)

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - name: 检出代码
        uses: actions/checkout@v4

      - name: 配置 Node.js 环境
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: 安装项目依赖
        run: npm ci

      - name: 安装 Playwright 浏览器
        run: npx playwright install --with-deps chromium

      - name: 运行端到端测试
        env:
          # 从 GitHub Secrets 注入 API Key,切勿明文写入代码
          GRIDINBOX_API_KEY: ${{ secrets.GRIDINBOX_API_KEY }}
          # 指向 Staging 测试环境
          APP_URL: https://staging.yourapp.com
          # Staging 专属测试收件箱 ID
          TEST_MAILBOX_ID: ${{ secrets.TEST_MAILBOX_ID }}
        run: npx playwright test --reporter=html

      - name: 上传 Playwright 测试报告
        uses: actions/upload-artifact@v4
        if: always()  # 即使测试失败也上传报告
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7  # 保留 7 天

以下几条最佳实践能让你的 CI 流水线更加稳定可靠:

  • 每个环境使用独立收件箱。不要在 Staging 和生产测试之间共享同一个收件箱——你可能会读到错误环境的 OTP。
  • received_at 时间过滤。轮询时,只接受测试开始之后收到的邮件。在 API 调用中加入 since 时间戳参数,避免读取上次测试遗留的旧 OTP。
  • 设置充裕的超时时间。Staging 环境的邮件投递有时会慢。建议使用 30 秒超时配合 2 秒轮询间隔,既安全又能在作业超时前完成。
  • 定期轮换 API Key。GRIDINBOX_API_KEY 像对待其他密钥一样管理——按计划轮换,并在 GridInbox 控制台审计访问日志。

总结

手动复制粘贴 OTP 是 CI/CD 流水线的隐形杀手。表面上看不起眼,但随着团队迭代节奏加快,这一个手动步骤会逐渐演变成每日瓶颈——更糟糕的是,当测试人员忘记及时检查收件箱时,还会带来不稳定的测试结果(Flaky Tests)。

有了提供解析后 OTP 字段的收件箱 API,这个缺口可以彻底弥合。流水线触发邮件发送、轮询获取验证码、填入表单、完成验证——全部自动完成,耗时仅相当于你拿起手机解锁屏幕的时间。

GridInbox 提供专属收件箱、服务端 OTP 解析、简洁的 REST API 以及租户级隔离——将邮件 OTP 自动化从一个需要数周的基础设施项目,变成一个下午就能落地的功能。

告别手动复制验证码

GridInbox 为你的 CI/CD 流水线提供真实收件箱与服务端 OTP 解析。几分钟完成配置,兼容任意测试框架,零基础设施负担。

免费开始使用 →