Email App
A framework-shipped App for a mailbox under test — verify a signup confirmation mail, read a one-time passcode, or follow a confirmation link an application under test sent, over a unified Microsoft-Graph + Gmail client.
← Back to overview · 🇩🇪 Deutsch · ← FTP App · Demo Web App →
What the Email App provides
One ready-to-use App, _Email, ships alongside _Skeleton, _ExampleWebApp, the OS App, and the FTP App — pick it in the scaffold wizard, or add its re-export by hand to an existing project. It wraps both Microsoft Graph (Microsoft 365/Outlook mailboxes) and the Gmail API behind one unified set of steps, so the same TestCase code works regardless of which provider the target mailbox lives on.
Reach for it whenever a flow under test sends an email and your test needs to look inside a real mailbox rather than stub the message out — for example:
- Confirming a signup flow actually sent a confirmation mail, then reading the one-time passcode (OTP) out of it to complete the flow
- Following a confirmation/magic link embedded in a password-reset or email-verification message
- Asserting that a notification email was (or was not) sent, without a human ever opening the mailbox
_Email is an ordinary App in the three-layer sense described in Concepts — a Control, TestSteps, and a References.ts barrel — the only difference from an App you’d write yourself is that the framework ships it pre-instantiated. All 12 TestSteps are named TS_Main_Email_<Action>, per the usual naming convention.
Import the Email Control into your test
Your project’s 2_Apps/1_Global/References.ts re-exports the App’s barrel, exactly like any other scaffolded App:
// 2_Apps/1_Global/References.ts
export * as Email from '../_Email/References';
With that in place, every step is callable from a TestCase through Project:
// 3_Cases/TC_MyFlow.spec.ts
import * as Project from '@GlobalRef';
await Project.Email.TS_Main_Email_CheckMessageExists('inbox', { subject: 'Welcome' }, true);
Like the OS App and FTP App, these steps take only their own business parameters — no leading refId, and no pageLogName/sectionName. None of the Email steps address a screen element, so there is no reference screenshot to bind and no page context to log — the connection alias you chose at Connect time is simply the step’s first argument.
(A) Register an Outlook account, step by step
TS_Main_Email_Connect accepts an auth argument with two shapes — this walkthrough covers the self-managed OAuth path, where the App acquires and refreshes its own access tokens against a mailbox you register in Entra ID (Azure AD). If a companion tool in your test setup already runs its own login and can hand you a token directly, skip ahead to Already have a token? Bring your own instead — there is no Entra ID setup on that path at all.
- Register an application in Entra ID (Azure AD) — see Microsoft’s own quickstart.
- Under API permissions, add the Microsoft Graph application permission
Mail.Read(addMail.Sendtoo if your tests send messages) — see Graph’s mail permissions reference. - Grant admin consent for the tenant — application permissions don’t take effect without it.
- Under Certificates & secrets, create a new client secret and copy its value immediately — Azure shows it only once.
- Note the Application (client) ID and Directory (tenant) ID from the app registration’s Overview page.
- Store the client secret in the password manager rather than hard-coding it —
npx cc-testframework password set --username email-oauth-client-secret --stdin(see Add a credential). - Connect with the app-only client-credential flow, reading the secret back from the password manager:
import * as Core from '@Core/References';
const clientSecret = await Core.PasswordManager.getPassword('email-oauth-client-secret');
await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, {
kind: 'oauth',
clientId: '<your-client-id>',
clientSecret,
tenantId: '<your-tenant-id>',
});
Omitting refreshToken (as above) uses the app-only client-credential flow. To use the per-mailbox delegated flow instead, complete a delegated OAuth consent flow once to obtain a refreshToken, store it the same way, and add it to the auth object. Either way, clientSecret, refreshToken, and any BYOT accessToken are never logged, never included in an error message, and never written into the test report.
Already have a token? Bring your own instead
If something in your test setup already runs its own OAuth flow — a companion tool driving a real login, or a token you mint yourself for a service account — hand the App the resulting access token directly and skip the registration above entirely. There is no project OAuth setup at all on this path, and the App never refreshes the token; you own its freshness for the duration of the test:
await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, {
kind: 'accessToken',
accessToken: token,
});
Using Gmail instead of Outlook
The same TS_Main_Email_Connect call works against a Gmail mailbox — swap 'microsoft' for 'google' and register a Google Cloud project instead of an Entra ID app:
- Create (or select) a project in the Google Cloud Console.
- Enable the Gmail API for that project.
- Configure the OAuth consent screen, then create an OAuth 2.0 Client ID — see Google’s OAuth 2.0 documentation.
- Run the consent flow once, with the Gmail scope your tests need (e.g.
gmail.readonlyto only read,gmail.modifyto also mark-read/delete,gmail.sendto send), to obtain a refresh token. - Store
clientId/clientSecret/refreshTokenin the password manager and supply all three — unlike Microsoft, Google’s flow has no app-only equivalent, sorefreshTokenis always required.
(B) Use the Email steps in a test
Once a mailbox is registered under an alias, every other TS_Main_Email_* step takes that alias as its first argument — connect, wait for a message, extract a code, and disconnect read like a short script:
await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, auth);
const msg = await Project.Email.TS_Main_Email_WaitForMessage('inbox', { from: 'noreply@<your-app-domain>', subject: 'Confirm' });
const otp = await Project.Email.TS_Main_Email_ExtractOtp('inbox', msg.id, /\d{6}/);
expect(otp).toHaveLength(6);
await Project.Email.TS_Main_Email_Disconnect('inbox');
The sections below cover each of the 12 TS_Main_Email_* steps in full, plus a complete TestCase wrapping them in numbered step blocks.
Connect once, operate on an alias, disconnect when done
Like FtpClient, a mailbox connection is session-based — the alias you choose at Connect time is what every later step takes as its first argument, so there is no live connection handle for your TestCase code to hold onto or pass around:
// Open a connection once, under a name you choose
await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, auth);
// ... run any number of operations against the same alias ...
const message = await Project.Email.TS_Main_Email_WaitForMessage('inbox', { subject: 'Welcome' });
// Close it when the flow is done
await Project.Email.TS_Main_Email_Disconnect('inbox');
Every mailbox operation is a standalone REST call rather than a persistent socket, but an unclosed alias still leaks for the rest of the test run — close it in a teardown hook rather than only at the end of the happy path:
// 3_Cases/TC_SignupConfirmation.spec.ts
import * as Project from '@GlobalRef';
test.afterEach(async () => {
await Project.Email.TS_Main_Email_Disconnect('inbox');
});
Find and read messages
TS_Main_Email_FindMessages, TS_Main_Email_WaitForMessage, TS_Main_Email_GetMessageText, and TS_Main_Email_GetMessageHtml return the value they fetch, in addition to recording non-secret metadata in the test report:
| Step | Returns | What it does |
|---|---|---|
TS_Main_Email_FindMessages | EmailMessage[] | Finds every message matching a query, records the match count |
TS_Main_Email_WaitForMessage | EmailMessage | Polls until a matching message arrives (or an optional timeout elapses), records its id/subject |
TS_Main_Email_GetMessageText | string | Fetches a message’s plain-text body, records its length |
TS_Main_Email_GetMessageHtml | string | Fetches a message’s HTML body, records its length |
A query is a plain object — { from?, subject?, since?, unreadOnly? } — every field optional and combinable:
// Wait up to 30s (the default) for the confirmation mail to arrive
const message = await Project.Email.TS_Main_Email_WaitForMessage('inbox', {
from: 'noreply@<your-app-domain>',
subject: 'Confirm your account',
});
// Or find every unread message from a sender, without waiting
const unread = await Project.Email.TS_Main_Email_FindMessages('inbox', { from: 'billing@<your-app-domain>', unreadOnly: true });
const body = await Project.Email.TS_Main_Email_GetMessageText('inbox', message.id);
Extract a one-time code or confirmation link
TS_Main_Email_ExtractOtp and TS_Main_Email_ExtractLink pull a substring out of a message’s plain-text body via a regular expression, and return it — but deliberately do not log the extracted value itself, only the messageId it came from, since both a passcode and a link (which frequently embeds its own token) are secrets:
// A 6-digit OTP embedded anywhere in the message text
const otp = await Project.Email.TS_Main_Email_ExtractOtp('inbox', message.id, /\d{6}/);
expect(otp).toHaveLength(6);
// The first confirmation link — pattern is optional, defaults to the first http(s) URL
const link = await Project.Email.TS_Main_Email_ExtractLink('inbox', message.id, /https:\/\/<your-app-domain>\/confirm\?[^\s]+/);
💡 Both extractors accept a capturing group. Pass a pattern with one capture group (e.g.
/code: (\d{6})/) to extract just that group instead of the whole match — useful when the code is embedded in surrounding text rather than standing alone.
Send and manage messages
| Step | What it does |
|---|---|
TS_Main_Email_SendMessage | Sends a new message (to, subject, body) |
TS_Main_Email_MarkRead | Marks a message as read |
TS_Main_Email_DeleteMessage | Deletes (moves to trash) a message |
await Project.Email.TS_Main_Email_SendMessage('inbox', 'qa@<your-app-domain>', 'Automated test message', 'Sent by the test suite');
await Project.Email.TS_Main_Email_MarkRead('inbox', message.id);
await Project.Email.TS_Main_Email_DeleteMessage('inbox', message.id);
Assert against the inbox
await Project.Email.TS_Main_Email_CheckMessageExists('inbox', { subject: 'Welcome' }, true);
await Project.Email.TS_Main_Email_CheckMessageExists('inbox', { subject: 'Account deleted' }, false);
TS_Main_Email_CheckMessageExists asserts whether a message matching a query exists (or not, per an expected boolean) — the same query shape used by TS_Main_Email_FindMessages above.
A full worked TestCase
Putting connect, a wait-for-message, an OTP extraction, and disconnect together in one flow:
// 3_Cases/TC_SignupConfirmation.spec.ts
import * as Project from '@GlobalRef';
Project.Core.test('TC_SignupConfirmation', async () => {
Project.Core.Step.setCurrentTestCaseName('TC_SignupConfirmation');
await Project.Core.Step.numberedStepBlock('Connect to the test inbox', async () => {
await Project.Email.TS_Main_Email_Connect('inbox', 'microsoft', mailbox, { kind: 'accessToken', accessToken: token });
});
await Project.Core.Step.numberedStepBlock('Sign up and read the confirmation code', async () => {
await Project.<YourApp>.TS_Execution_Start();
await Project.<YourApp>.TS_Main_Button_Click(pageLogName, sectionName, 'Sign up');
const message = await Project.Email.TS_Main_Email_WaitForMessage('inbox', { from: 'noreply@<your-app-domain>', subject: 'Confirm' });
const otp = await Project.Email.TS_Main_Email_ExtractOtp('inbox', message.id, /\d{6}/);
expect(otp).toHaveLength(6);
});
await Project.Core.Step.numberedStepBlock('Disconnect', async () => {
await Project.Email.TS_Main_Email_Disconnect('inbox');
});
});
Provider reference
provider value | API | Auth flows supported |
|---|---|---|
microsoft | Microsoft Graph (Mail.Read/Mail.Send, .default scope) | App-only client-credential (omit refreshToken), or delegated refresh-token flow (supply refreshToken) |
google | Gmail API (gmail.readonly/gmail.modify/gmail.send scope, chosen at consent time) | Refresh-token flow only — Google’s flow has no app-only equivalent, so refreshToken is always required |
Dependencies ship with the framework
@azure/msal-node and google-auth-library are regular dependencies of the framework itself, not optional add-ons — installing the framework already installs both, so there is no extra setup step for either provider’s OAuth library.
Live tests vs. unit tests
A live smoke test against this App needs a real mailbox and real OAuth credentials (or a real BYOT access token) — there is no way around actually sending and receiving mail through the provider’s own servers. The framework’s own test suite for this App runs entirely against fakes instead, so it never depends on network access, a real Microsoft/Google account, or a live inbox; your project’s tests are expected to do the same for anything that doesn’t need the real mailbox, and reserve the real connection for the handful of scenarios that specifically verify email delivery.
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
No Email connection registered for alias '...' | An operation step ran before TS_Main_Email_Connect for that alias, or after TS_Main_Email_Disconnect already closed it | Call Connect first, and check afterEach/afterAll ordering isn’t disconnecting too early |
Authentication failed for mailbox '...': token expired or invalid | A BYOT access token expired, or the OAuth client credentials are wrong | Supply a fresh access token, or re-check clientId/clientSecret/tenantId/refreshToken against the provider’s own app registration |
No message ever matches TS_Main_Email_WaitForMessage’s query | The mail hasn’t arrived yet (raise timeoutMs), the query is too narrow, or the App under test never actually sent it | Widen the query first to confirm anything arrives at all, then narrow it back down; check the App under test’s own mail-sending path independently |
| Google OAuth requires a refreshToken | auth.refreshToken was omitted on a google connection | Complete the consent flow once to obtain a refresh token — see Using Gmail instead of Outlook above |
Where to go next
- Credential Management — storing
clientSecret/refreshTokeninstead of hard-coding them - OS App / FTP App — the other framework-shipped utility Apps
- Build TestSteps — the general
TS_Main_*naming convention these steps follow - FAQ — troubleshooting setup issues
📧 Questions? Contact: jens.szelag@itsbusiness.ch
itsbusiness AG · Bern · Switzerland