otp extraction, email parsing, security, automation, testing

OTP Extraction: A More Reliable Approach Than Regex Alone

A six-digit regex is easy to write and easy to fool. Build a layered OTP parser using context scoring, format awareness, false-positive filters, safe HTML handling, and a labeled test corpus.

A one-time passcode often looks like the easiest thing in an email: find six digits and return them. That shortcut fails on order numbers, dates, phone fragments, support PINs, and messages containing several plausible codes. It also fails when a code is split across HTML elements or formatted with spaces.

Reliable OTP extraction is a classification problem with a deterministic core. Relay can provide the message text through its REST API, but OTP extraction belongs in your application. A layered parser can produce a candidate, a confidence score, and an explanation instead of silently guessing.

Verification email highlighting one unambiguous OTP among rejected patterns

Define the decision before writing patterns

Decide what your workflow should do when more than one code appears. Login automation may require the newest message from a known sender, while a human review queue may accept ambiguous results. A parser should return found, ambiguous, or not_found, not force every input into a string.

Start with explicit constraints: expected length, accepted alphabets, validity window, sender policy, and nearby keywords. A six-digit numeric code and an eight-character alphanumeric code should be different parser configurations. Avoid a universal expression that accepts every token from four to twelve characters.

Signal Positive example Caution
Purpose phrase 'verification code' 'security alert' alone is weak
Proximity code on the next line distant footer numbers are risky
Format six isolated digits grouped digits may be order IDs
Expiration 'expires in 10 minutes' not all OTPs include it
Sender expected service domain sender data can be spoofed

Use the Relay API to retrieve the current message and apply your own sender and freshness policy. Never treat extraction alone as proof that a login attempt is legitimate.

Build a layered extraction pipeline

A practical pipeline has five stages: normalize, tokenize, generate candidates, reject known negatives, and score the remainder. Preserve offsets during normalization so you can inspect the original context. Decode HTML entities before text analysis, normalize Unicode carefully, and convert nonbreaking spaces to ordinary spaces.

The following example operates on message text returned by your Relay REST client. It intentionally favors explainability over a single compact expression.

const POSITIVE = ['verification code', 'one-time passcode', 'login code', 'security code'];
const NEGATIVE = ['order number', 'invoice', 'tracking number', 'customer id', 'phone'];

function extractOtp(text) {
  const normalized = text.replaceAll('\u00a0', ' ').replace(/[ \t]+/g, ' ');
  const pattern = /(?:^|[^0-9])([0-9]{6})(?![0-9])/g;
  const candidates = [];

  for (const match of normalized.matchAll(pattern)) {
    const value = match[1];
    const start = match.index + match[0].indexOf(value);
    const context = normalized.slice(Math.max(0, start - 100), start + 106).toLowerCase();
    let score = 0;
    if (POSITIVE.some((phrase) => context.includes(phrase))) score += 4;
    if (/expires|valid for/.test(context)) score += 1;
    if (NEGATIVE.some((phrase) => context.includes(phrase))) score -= 5;
    candidates.push({ value, start, score });
  }

  const viable = candidates.filter((candidate) => candidate.score >= 4);
  if (viable.length === 1) return { status: 'found', ...viable[0] };
  if (viable.length > 1) return { status: 'ambiguous', candidates: viable };
  return { status: 'not_found', candidates };
}

This is a starting point, not a universal model. Keep the threshold and vocabulary under version control, and return candidates without logging full message text.

Score context rather than trusting digit shape

Proximity matters. Weight a phrase in the same block more heavily than one two paragraphs away. Positive evidence might include 'enter this code', an expiration statement, or a known template. Negative evidence includes currency symbols, year-like prefixes, invoice labels, telephone formatting, and long runs of digits.

Support expected variations deliberately. Some providers render 123 456 or ABC-DEF2; normalize those only when your configuration allows the resulting alphabet and length. Do not remove every hyphen globally, because that can merge unrelated identifiers.

Resist using a language model as the first extractor for every message. A deterministic parser is cheaper to test and explains its decisions. If you add a model for ambiguous cases, constrain its output to existing candidates and require a reason code; never allow it to invent a value absent from the message.

Handle HTML and text without executing content

Prefer the plain-text representation when it preserves the code and context. If you inspect HTML, treat it as untrusted even though Relay sanitizes HTML. Do not inject it into a browser DOM, render remote images, or follow links as part of extraction. Parse it in an isolated library, remove style and script-like content, and extract visible text with stable block boundaries.

A code split as <strong>123</strong><strong>456</strong> may need concatenation, while separate table cells may need a delimiter. Record transformation rules and test them against real template shapes. Never run scripts, evaluate template expressions, or fetch external resources to make a message easier to parse.

The broader security model for agents is covered in Build an Email Automation Agent with MCP, especially the need to separate data from instructions.

Test with a corpus and protect the workflow

Create a labeled corpus from synthetic and appropriately authorized examples. Include empty mail, multiple codes, expired codes, internationalized text, nested HTML, receipts, dates, phone numbers, and adversarial phrases such as 'ignore previous rules'. Keep expected status and expected value separate so ambiguity is tested too.

Split the corpus by provider template and keep a holdout set that parser authors do not tune against. Otherwise, adding keywords for every failure can produce an impressive score on examples the parser has effectively memorized. Version fixtures alongside normalization rules so a change in HTML-to-text output cannot silently invalidate earlier results.

Measure false acceptance and false rejection separately. In account automation, accepting an invoice number can be more harmful than requesting review. Add regression cases whenever a parser misses a production template, but redact addresses and tokens before storing fixtures.

Operational controls matter as much as parsing. Bind a code to the intended inbox, sender policy, and login session; enforce a short expiry; and never replay a code after its session ends. Scope Relay keys narrowly, revoke unused keys in your account, and avoid persisting OTPs beyond the workflow that needs them.

Frequently asked questions

Does Relay automatically extract OTPs?

No. Relay provides access to inbox messages through REST and MCP. Your application performs extraction using message text or other returned content, with its own parsing and security policy.

Should I accept the highest-scoring candidate when scores tie?

Usually not. A tie indicates ambiguity, and guessing can submit the wrong credential to a sensitive workflow. Return an ambiguous result, narrow by trusted template or message context, or request human review.

Is plain text always safer and better than HTML?

Plain text is simpler to process safely, but it may omit structure that helps identify context. Use it first when adequate, and parse HTML only with isolated, non-executing tooling and explicit normalization tests.

Make extraction explainable and bounded

Good OTP automation does not rely on one expression. It combines constrained candidate generation, contextual evidence, explicit ambiguity, safe content handling, and regression testing. Relay supplies the message access layer; your parser owns the decision. Begin with one provider's templates, measure both error directions, and expand only when the corpus supports it. Use Relay's documentation to build that message pipeline without exposing credentials or turning email content into trusted instructions.

FROM THE COLLECTIONS

Continue with related guides

More practical reading on agent-friendly email, testing, privacy, and secure automation.