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.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallWhat 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.
- Create an isolated mailbox or test identity and register an application in the Microsoft identity platform.
- Acquire an OAuth 2.0 access token using the least-privileged delegated or application mail permission that permits the required read operation.
- Use Playwright to submit a signup form with a unique email address or run identifier.
- Poll the inbox through Graph, following paging links and matching sender, subject, recipient, and recency.
- Read the message body as text or HTML, extract only the expected code or URL, and complete verification in the browser.
- 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.
#1 Best Overall
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 --noEmitcheck, 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.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitches| 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.
Rank #2
- Create a run identifier at test start, for example
const runId = crypto.randomUUID();. - Build the test address from a mailbox strategy your tenant supports. If aliases are unavailable, use one dedicated address and put
runIdin a signup field that is reflected in the outgoing message. - Open the application with Playwright, fill the email and password fields, and submit the form.
- 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.
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.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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.
receivedDateTimeis 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.
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.
Rank #4
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.
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.
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.
Recommended Free Tools
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.
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.
Quick Recap
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.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →

