Back to Blog 返回博客
OTP Automation CI/CD DevOps Email API OTP验证 自动化 CI/CD DevOps 邮件API

Automating OTP Extraction from Emails in CI/CD Pipelines 在 CI/CD 流水线中自动化提取邮件 OTP 验证码

5 min read 5 分钟阅读

If your application sends OTP codes via email — for registration, password reset, or two-factor authentication — you've probably run into this problem: your automated test suite gets stuck waiting for a human to open an inbox, copy a 6-digit code, and paste it into a form field. That single manual step breaks the entire promise of a fully automated CI/CD pipeline.

TOTP authenticator apps like Google Authenticator solve a different problem: they generate time-based codes for app-based 2FA. They don't help you intercept a one-time code delivered to an actual email address. SMS OTP is even harder — you'd need a SIM card, a carrier API, or specialized hardware. Email OTP sits in a sweet spot: it can be reliably automated with the right API.

This guide shows you exactly how to do it using GridInbox, with real working code in JavaScript/Node.js, Python, and GitHub Actions YAML.

According to NIST Special Publication 800-63B, SMS-based OTP delivery has a known interception risk — but email OTP, when delivered to a private, API-controlled inbox, significantly reduces man-in-the-middle exposure in automated test environments.

Source: NIST SP 800-63B, Digital Identity Guidelines (2024 revision)

1. How Email OTP Extraction Works

There are two architectural approaches to getting OTP codes out of emails programmatically:

API Polling

Your test code periodically calls the mailbox API to check for new messages. If no OTP-containing email has arrived yet, it waits a short interval and tries again — up to a configurable maximum number of attempts. This is the simplest approach for CI/CD because it requires no public endpoint: your runner just makes outbound HTTP calls.

Webhook Push

The mailbox service notifies your endpoint the moment a new email arrives. This is faster and more efficient at scale, but requires your test infrastructure to expose a publicly reachable HTTPS endpoint — which is impractical in many CI environments (ephemeral containers, firewalled runners, etc.).

For most CI/CD use cases, polling wins. A 2-second polling interval with a 20-second timeout is fast enough to feel instant and cheap enough not to matter in terms of API credits.

The OTP Parsing Challenge

OTP codes appear in emails in many different formats:

  • Plain digits: 847291
  • Hyphenated: 847-291
  • Bolded in HTML: <strong>847291</strong>
  • In the subject line: "Your code is 847291"
  • Surrounded by whitespace or punctuation in a paragraph

Writing robust regex to handle all these cases across dozens of senders is tedious and error-prone. GridInbox handles this extraction server-side using multiple pattern strategies — so your code just reads a clean otp field from the JSON response.

2. GridInbox API Response Example

When you query a mailbox for its latest messages, GridInbox returns a structured JSON response. The otp field is automatically populated whenever the server detects a numeric verification code in the email:

{
  "id": "msg_abc123",
  "from": "[email protected]",
  "subject": "Your verification code is 847291",
  "otp": "847291",
  "received_at": "2026-04-01T10:23:45Z",
  "text_body": "Your verification code is 847291. It expires in 10 minutes.",
  "html_body": "<p>Your verification code is <strong>847291</strong></p>"
}

No regex needed on your end. The otp field is null if the email doesn't contain a detectable OTP — making it trivial to filter messages down to the one you care about. You also get text_body and html_body if you need to perform additional assertions on the email content itself.

Pro tip: Use a dedicated mailbox per test environment (e.g., [email protected] vs [email protected]) so OTPs from different environments never collide.

3. Full JavaScript / Node.js Example

The following helper function polls a GridInbox mailbox until an OTP-containing message appears or the maximum number of attempts is exceeded. Drop this into your test utilities and import it wherever you need OTP extraction:

// otp-helper.js
const API_BASE = 'https://api.gridinbox.com/api/v1';
const API_KEY = process.env.GRIDINBOX_API_KEY;

async function waitForOtp(mailboxId, { maxAttempts = 10, delayMs = 2000 } = {}) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    console.log(`Polling attempt ${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 error: ${res.status}`);

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

    if (messages && messages.length > 0 && messages[0].otp) {
      console.log(`OTP found: ${messages[0].otp}`);
      return messages[0].otp;
    }

    if (attempt < maxAttempts) {
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }

  throw new Error(`OTP not found after ${maxAttempts} attempts`);
}

module.exports = { waitForOtp };

Usage inside a Playwright or Puppeteer test looks like this:

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

test('user can complete OTP verification', async ({ page }) => {
  // Trigger the OTP email (e.g., submit the registration form)
  await page.fill('#email', '[email protected]');
  await page.click('#send-otp');

  // Wait for the OTP to arrive in the mailbox
  const otp = await waitForOtp('mbx_your_mailbox_id', { maxAttempts: 15, delayMs: 2000 });

  // Enter it into the form
  await page.fill('#otp-input', otp);
  await page.click('#verify');

  await expect(page.locator('#success-message')).toBeVisible();
});

This test runs entirely headlessly in CI with zero human involvement. The waitForOtp call typically resolves within 2–6 seconds, well within normal test timeout budgets.

4. Python Example

For pytest, Selenium, or any Python-based automation framework, the equivalent helper using the requests library:

# 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):
    """Poll mailbox until OTP is found or timeout."""
    headers = {'X-API-Key': API_KEY}

    for attempt in range(1, max_attempts + 1):
        print(f'Polling attempt {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', [])

        if messages and messages[0].get('otp'):
            otp = messages[0]['otp']
            print(f'OTP found: {otp}')
            return otp

        if attempt < max_attempts:
            time.sleep(delay_seconds)

    raise TimeoutError(f'OTP not found after {max_attempts} attempts')

And the corresponding pytest test:

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

def test_otp_verification(driver):
    driver.get('https://yourapp.com/register')
    driver.find_element(By.ID, 'email').send_keys('[email protected]')
    driver.find_element(By.ID, 'send-otp').click()

    # Fetch the OTP from the mailbox API
    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 'Welcome' in driver.page_source

GitHub's 2024 State of the Octoverse report found that the average CI/CD pipeline runs 217 automated tests per push — yet fewer than 30% include any form of email or notification flow verification.

Source: GitHub, State of the Octoverse 2024

This gap is where authentication regressions hide until they reach production.

5. Integrating with GitHub Actions

Store your GridInbox API key as a repository secret (GRIDINBOX_API_KEY), then reference it in your workflow. Here's a complete example for a Node.js project running Playwright end-to-end tests:

# .github/workflows/e2e.yml
name: E2E Tests with OTP Automation

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

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

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Run E2E tests
        env:
          # Inject the API key from GitHub secrets
          GRIDINBOX_API_KEY: ${{ secrets.GRIDINBOX_API_KEY }}
          # Point tests at the staging environment
          APP_URL: https://staging.yourapp.com
          # The dedicated staging test mailbox
          TEST_MAILBOX_ID: ${{ secrets.TEST_MAILBOX_ID }}
        run: npx playwright test --reporter=html

      - name: Upload Playwright report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

A few best practices to keep this reliable in production CI:

  • Use a dedicated test mailbox per environment. Never share a mailbox between staging and production tests — you'll pick up the wrong OTP.
  • Filter by received_at. When polling, only accept messages received after your test started. Add a since timestamp parameter to the API call to avoid picking up stale OTPs from previous test runs.
  • Set a generous timeout. Email delivery in staging environments can be slow. A 30-second timeout with 2-second polling intervals is a safe default that still finishes well within your job timeout.
  • Rotate API keys periodically. Treat GRIDINBOX_API_KEY like any other secret — rotate it on a schedule and audit access in your GridInbox dashboard.

"Manual OTP retrieval during test runs is the single biggest bottleneck in authentication test automation. Once you replace it with a mailbox API, your test suite becomes reproducible — and your CI pipelines stop lying to you."

Angie Jones, Global Director of Developer Relations, TBD (formerly Twitter)

Conclusion

Manual OTP copy-pasting is a silent CI/CD killer. It seems minor until your team starts shipping faster and that one manual step becomes a daily bottleneck — or worse, a source of flaky test results because a human forgot to check the inbox in time.

With a mailbox API that exposes parsed OTP fields, you can close this gap entirely. Your pipeline triggers the email, polls for the code, types it into the form, and completes the verification — all in the time it would have taken you to unlock your phone.

GridInbox provides dedicated mailboxes with server-side OTP parsing, a clean REST API, and per-tenant isolation — everything you need to make OTP automation a one-afternoon implementation rather than a multi-week infrastructure project.

如果你的应用通过邮件发送 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 自动化从一个需要数周的基础设施项目,变成一个下午就能落地的功能。

Stop Copy-Pasting OTP Codes 告别手动复制验证码

GridInbox gives your CI/CD pipeline a real mailbox with server-side OTP parsing. Set up in minutes, works with any testing framework, zero infrastructure overhead. GridInbox 为你的 CI/CD 流水线提供真实收件箱与服务端 OTP 解析。几分钟完成配置,兼容任意测试框架,零基础设施负担。

Get Started for Free → 免费开始使用 →