在 CI/CD 流水线中自动化提取邮件 OTP 验证码
如果你的应用通过邮件发送 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_body 和 html_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 自动化从一个需要数周的基础设施项目,变成一个下午就能落地的功能。