DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
FREEDOM251

Freedom251 › Blog › email verification

How to Test Playwright Email Verification with Microsoft Graph

By Freedom251 Team · · 11 min read

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Automate the whole verification journey by letting Playwright drive the signup page while Microsoft Graph reads a dedicated test mailbox. The test submits a unique address, obtains an OAuth 2.0 token with the narrowest mail permission it needs, polls GET /me/mailFolders/inbox/messages (or GET /me/messages), extracts the verification code or URL, and finishes the flow in the browser. This avoids brittle human inbox checks and keeps mailbox access inside your test run.

Delivery time depends on your mail system and test environment; Microsoft’s published guidance does not define a universal verification-email SLA. Design polling with a bounded timeout, precise message matching, safe token handling, and cleanup rather than assuming an instant message.

As an Amazon Associate I earn from qualifying purchases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What does a Playwright–Microsoft Graph email test look like?

The reliable pattern separates browser work from mailbox work. Playwright submits the registration form and opens the verification link, while an HTTP client calls Microsoft Graph to inspect the test mailbox. The browser and API portions share a unique run identifier, so a message from another test cannot satisfy the assertion.

  1. Create an isolated mailbox or test identity and register an application in the Microsoft identity platform.
  2. Acquire an OAuth 2.0 access token using the least-privileged delegated or application mail permission that permits the required read operation.
  3. Use Playwright to submit a signup form with a unique email address or run identifier.
  4. Poll the inbox through Graph, following paging links and matching sender, subject, recipient, and recency.
  5. Read the message body as text or HTML, extract only the expected code or URL, and complete verification in the browser.
  6. Assert the application’s verified state, then remove or archive test messages when your mailbox policy allows it.

This arrangement also lets you validate mailbox state directly with Playwright’s APIRequestContext, which can send HTTP(S) requests for setup and post-condition checks.

What do I need before writing the test?

Prepare the identity, permissions, and test data before adding selectors or polling code. Keeping these concerns separate makes failures diagnosable.

Mailbox and tenant

  • Use a dedicated test mailbox or an isolated test identity. Do not point automated tests at a personal or production inbox.
  • Know the tenant that owns the mailbox and the address the application will send to.
  • Decide whether tests may delete messages. If deletion is prohibited, use a dedicated folder or an archive rule instead.

Application registration

  • Register an app in the Microsoft identity platform and record its client ID and tenant information.
  • Choose delegated access when a signed-in test user grants access, or application access when a service process runs without an interactive user.
  • Have an administrator grant consent when your tenant requires it. Store secrets in the CI secret store, never in the repository.

Playwright project

  • Playwright supports TypeScript out of the box. Add a separate tsc --noEmit check, as recommended in Playwright documentation, so type errors are caught independently of the test runner.
  • Keep the Graph token and mailbox address in environment variables or your CI secret manager.
  • Generate a run-specific marker, such as a UUID, and include it in the address alias when your mail setup supports aliases, or in a field that reaches the message subject or body.

Which Graph authentication model should the test use?

Delegated and application permissions solve different execution models. Select the smallest permission and the identity boundary that fits your pipeline.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Model How it runs Operational considerations
Delegated A user signs in and the token acts on that user’s behalf. Useful for locally run or interactive suites. User consent, conditional-access rules, and token refresh can affect unattended jobs.
Application A service authenticates without a signed-in user and accesses mail allowed by tenant policy. Better suited to headless CI, but requires administrator consent and careful restriction of which mailboxes the app can reach.

Microsoft Graph mail access is OAuth-protected. Use the identity platform’s OAuth 2.0 flow and request only the mail permission that permits the messages your test must read. A token that lacks the required scope or role will produce an authorization error; adding broad permissions is not a substitute for correcting consent or tenant configuration.

Keep token acquisition outside test assertions

Wrap token acquisition in a fixture or helper that returns a short-lived access token to the test. Do not print the token, authorization header, message body, or verification URL in failure output. If a setup request fails, report the HTTP status and a redacted diagnostic rather than the bearer credential.

How do I submit a signup with a unique test address?

Make each registration distinguishable before you touch the inbox. A unique marker prevents a delayed message from an earlier run from passing the current test.

  1. Create a run identifier at test start, for example const runId = crypto.randomUUID();.
  2. Build the test address from a mailbox strategy your tenant supports. If aliases are unavailable, use one dedicated address and put runId in a signup field that is reflected in the outgoing message.
  3. Open the application with Playwright, fill the email and password fields, and submit the form.
  4. Capture the time immediately before submission. Use that timestamp as a lower bound when matching messages, allowing for a small clock-skew margin between systems.
const submittedAt = Date.now();
await page.getByLabel('Email').fill(testAddress);
await page.getByLabel('Password').fill(testPassword);
await page.getByRole('button', { name: /sign up/i }).click();

Do not rely on an inbox’s current first row or on a fixed subject alone. Parallel workers can deliver messages in a different order, and a retry can leave more than one valid-looking email.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do I poll Microsoft Graph for the verification message?

Request only the fields needed for matching and extraction, then repeat the request until a bounded deadline. Folder-specific retrieval is clearer when you know the message is delivered to Inbox; /me/messages is useful when a mail rule may move it.

Use a narrow message query

The message-list resources are GET /me/mailFolders/inbox/messages and GET /me/messages. Add $select for fields such as id, receivedDateTime, subject, from, toRecipients, body, and bodyPreview. OData filters can reduce the candidate set, but keep a client-side check as well because sender formatting and server-side filtering behavior can vary.

const endpoint = new URL('https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages');
endpoint.searchParams.set('$select', 'id,receivedDateTime,subject,from,toRecipients,body,bodyPreview');
endpoint.searchParams.set('$orderby', 'receivedDateTime desc');
endpoint.searchParams.set('$top', '25');

Use the v1.0 Graph endpoint for production code where the resource is available. A Microsoft page for list-messages beta warns that beta APIs can change, so do not make a production test depend on beta behavior without an explicit compatibility decision.

Follow every page

Graph may return an @odata.nextLink property. Treat that value as an opaque, complete URL and request it exactly as returned; do not rebuild it from the original path or drop its query parameters. A message on a later page is still a valid candidate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
async function readAllPages(request: APIRequestContext, token: string, firstUrl: string) {
  const messages: unknown[] = [];
  let url: string | undefined = firstUrl;
  while (url) {
    const response = await request.get(url, {
      headers: { Authorization: `Bearer ${token}` }
    });
    if (!response.ok()) throw new Error(`Graph mail read failed: ${response.status()}`);
    const page = await response.json();
    messages.push(...(page.value ?? []));
    url = page['@odata.nextLink'];
  }
  return messages;
}

In a real helper, type the response shape instead of using unknown[], and cap the number of pages or candidates you process during one poll so a misconfigured mailbox cannot create unbounded work.

Match more than one field

Require several independent signals before accepting a message:

  • The recipient matches the address or run-specific alias used by the test.
  • The sender matches the application’s expected sender address or domain.
  • The subject contains the expected verification wording, with case and whitespace normalized.
  • receivedDateTime is after the submission lower bound, with an allowance for known clock skew.
  • The body contains the run marker, expected account identifier, or another test-specific value when your application includes one.

Normalize addresses and subject text before comparison, but do not accept a partial URL or arbitrary six-digit number merely because it appears in a message. A delayed message from another test must fail the complete predicate.

How should the polling loop handle delivery delays?

Use a deadline and a short, increasing delay rather than a tight loop. There is no universal delivery-time guarantee, so the timeout belongs in your environment configuration and should be long enough for your mail path without hiding a broken sender.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
async function waitForMessage(
  fetchCandidates: () => Promise<Message[]>,
  isMatch: (message: Message) => boolean,
  timeoutMs = 120_000
) {
  const deadline = Date.now() + timeoutMs;
  let delayMs = 1_000;
  while (Date.now() < deadline) {
    const candidates = await fetchCandidates();
    const match = candidates.find(isMatch);
    if (match) return match;
    await new Promise(resolve => setTimeout(resolve, delayMs));
    delayMs = Math.min(Math.round(delayMs * 1.5), 10_000);
  }
  throw new Error('Verification message was not received before the test deadline');
}

When Graph responds with throttling or a transient server error, honor its retry guidance when present and increase the delay. When the deadline expires, capture only redacted diagnostics: candidate count, statuses, and timestamps. Never attach the complete message body or verification link to a CI artifact.

How do I extract a verification code or link safely?

Prefer a narrowly scoped parser tied to your application’s format. Request the body in text or HTML, then validate the extracted value before using it.

Code-based verification

For a fixed six-character format, anchor the expression to the label or surrounding text your application emits, and reject multiple matches. Convert the result to the exact string expected by the form; do not silently trim meaningful characters.

const codeMatch = bodyText.match(/verifications+codes*[:#-]?s*([A-Z0-9]{6})/i);
if (!codeMatch) throw new Error('Expected verification code was not found');
await page.getByLabel('Verification code').fill(codeMatch[1]);

Link-based verification

Parse anchor elements or URLs from the HTML, then allow only the application’s expected HTTPS origin and path. Reject links with unexpected hosts, schemes, or query parameters. Opening an arbitrary URL found in an email turns the test mailbox into a navigation injection point.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const verificationUrl = findAllowedVerificationUrl(html, {
  origin: 'https://app.example.test',
  pathPrefix: '/verify'
});
await page.goto(verificationUrl);

If the message is HTML, strip tags or use a real HTML parser before searching text. Keep a text-only fallback for messages whose body is converted to plain text by transport rules. The parser should return one value or fail loudly; choosing the first URL in a marketing footer is not verification.

How do I combine Graph requests with Playwright’s APIRequestContext?

Use the context for mailbox reads and application API setup while retaining the browser page for user-visible actions. This keeps authentication headers out of page JavaScript and makes API failures distinct from UI failures.

import { test, expect } from '@playwright/test';

test('verifies a new account from its email', async ({ page, request }) => {
  const token = await getGraphToken();
  const runId = crypto.randomUUID();
  const address = makeTestAddress(runId);
  const submittedAt = new Date();

  await page.goto('https://app.example.test/signup');
  await page.getByLabel('Email').fill(address);
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
  await page.getByRole('button', { name: /sign up/i }).click();

  const message = await waitForMessage(
    () => listInboxMessages(request, token),
    message => matchesRun(message, { address, runId, submittedAt })
  );

  const url = extractVerificationUrl(message);
  await page.goto(url);
  await expect(page.getByText(/account verified/i)).toBeVisible();
});

The sample separates token acquisition, listing, matching, and parsing into helpers so each can be unit-tested. If your application exposes a post-verification API state, an additional APIRequestContext assertion can confirm the account is verified without relying only on a success banner.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

What are the common failure modes and fixes?

Symptom Likely cause Fix
401 or 403 from Graph Expired token, missing consent, wrong tenant, or insufficient mail permission. Check the token audience and scopes or roles, renew the token, and have the correct administrator grant consent. Do not automatically request broad mail access.
No messages found Message is delayed, moved out of Inbox, or the test is querying the wrong mailbox. Verify the mailbox identity, try /me/messages when rules move mail, follow @odata.nextLink, and continue polling until the configured deadline.
Wrong message accepted Subject-only or newest-message matching. Require recipient, sender, subject, recency, and a run-specific marker where available.
Parser finds no code or URL HTML/plain-text conversion changed the body or the template changed. Request the needed body representation, parse HTML deliberately, keep a tested text fallback, and fail with redacted diagnostics.
Intermittent navigation failure Verification URL expired, host validation rejected it, or the application was not ready. Validate the URL against the expected origin, navigate through the normal browser context, and assert the resulting state rather than assuming a redirect completed.
Tests interfere with one another Shared mailbox, reused address, or stale messages. Use isolated identities or unique aliases, include a run marker, and clean up messages after each test or suite.

How should I secure and clean up the workflow?

Treat mail content as sensitive test data. Redact bearer tokens, authorization headers, message bodies, verification URLs, passwords, and full email addresses from traces and CI logs. Restrict mailbox access to the test application and rotate credentials according to your tenant policy.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

After assertions pass or fail, remove, archive, or label matching messages according to your organization’s retention rules. Cleanup should use the current message object returned by Graph. Message IDs can change after mailbox actions, so do not persist an ID as a permanent business key; identify messages with stable, test-specific metadata during the current retrieval.

Keep production code on Graph v1.0 where possible. Review tenant consent whenever permissions change, and test the delegated and application paths separately if both are supported by your pipeline.

What should I check before enabling this in CI?

  • The CI identity can obtain a token in the target tenant without an interactive prompt.
  • The app has only the mail permission required for message retrieval, with documented administrator consent where needed.
  • The mailbox is isolated from real users and has a defined retention or cleanup policy.
  • Every test uses a unique address or run marker and a complete message-matching predicate.
  • List responses are paged safely through the complete @odata.nextLink.
  • Polling has a configurable deadline, backoff, and handling for throttling or transient failures.
  • Code and link parsers reject malformed or unexpected values.
  • Logs and traces are redacted, and the TypeScript project passes tsc --noEmit.
  • The final assertion checks the application’s verified state, not merely that an email was received.

Which approach is best for a local run versus unattended CI?

A delegated token is often simplest while developing locally because it reflects a real user session. For unattended CI, an application identity avoids interactive sign-in but demands tighter tenant controls and administrator approval. In either case, mailbox isolation, precise matching, bounded polling, and secret redaction matter more than the choice of token flow alone.

Frequently Asked Questions

Can I read Outlook mail in Playwright without opening a browser tab?

Yes. Use Playwright’s APIRequestContext or another HTTPS client to call Microsoft Graph directly, then use the browser only for the signup and verification state. The Graph call still requires a valid OAuth 2.0 token and the appropriate tenant consent.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does my Graph query return only some messages?

Message collections are paged. Read the complete @odata.nextLink URL from each response, preserving its query parameters, until no next link remains; otherwise a matching email can be missed.

Is it safe to click the first link found in an email body?

No. Extract links with an application-specific parser and allow-list the expected HTTPS origin and path before navigation. This prevents a footer, tracking link, or unexpected content from redirecting the test.

Should I save a Microsoft Graph message ID between test runs?

No. Message IDs may change after mailbox operations and are not a durable business key. Re-query the mailbox and match the current result using the test address, sender, subject, recency, and run marker.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

email verificationEnd-to-End TestingMicrosoft GraphOAuth 2.0PlaywrightTypeScript

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.