Updated September 23, 2026 by Mailsac Engineering. Examples use Cypress 16 and @mailsac/cypress 0.1.0.
In short: Cypress can’t read email by itself. Mailsac gives your tests real inboxes at mailsac.com or your own test domain, and the @mailsac/cypress plugin waits for the email your test just triggered, then returns its reset link or verification code. The API key stays in Cypress’s Node process, never in the browser. New to this? See how the email testing API works.
Get a free API key → The free plan includes an API key, 1 private address and 1,500 operations a month. Team plans with shared custom domains start at $89/month.
Cypress email testing can cover the whole password-reset journey: request an email, receive it, open the correct link, set a new password, and sign in. The difficult part is making sure your test reads the message it just requested, rather than an older reset email or an unrelated notification.
The Mailsac Cypress integration handles that wait in Cypress’s Node process. Your application sends mail through its usual email provider; Mailsac receives it, and the integration reads the matching message through the Mailsac API. The API key stays out of the browser.
Start with the runnable example below, then connect the same pattern to your application. For real delivery, create a Mailsac account and use a test inbox whose message bodies your API key can read.
Run a complete password-reset test first
Use Node.js 22 or 24, the versions exercised in the package’s CI. Clone the published release and run:
git clone --branch v0.1.0 https://github.com/mailsac/cypress-mailsac.git
cd cypress-mailsac
npm ci
npm run test:e2e
The runner starts a small example app and a local Mailsac API fixture, runs Cypress headlessly, then stops the app. This default run needs no account, API key, or SMTP credentials. It sends no email and does not test delivery to the real Mailsac service.
The fixture deliberately contains an old password-reset email and a newer unrelated message. The correct email arrives after a delay. The test waits for it, follows its reset link, changes the password, verifies that the old password fails and the new password works, and checks that the reset link cannot be reused.
The example’s README also documents a separate SMTP mode. That mode sends a real message through your SMTP provider to a Mailsac inbox you control. Copy its .env.smtp.example to the ignored .env.smtp file, supply your SMTP settings and Mailsac key, then follow the two-terminal instructions. The app is a learning example with in-memory accounts, not a production authentication service.
Add Mailsac to your Cypress project
For the versions used in this guide:
npm install --save-dev cypress@16.1.0 @mailsac/cypress@0.1.0
Set MAILSAC_API_KEY in your local process environment or CI secret store. Do not prefix it with CYPRESS_, put it in Cypress.env() or config.env, or commit it.
Register the Node tasks in cypress.config.js. This example uses CommonJS configuration; merge these settings into your existing configuration and adjust baseUrl to your running app.
const { defineConfig } = require('cypress');
const { createMailsacTasks } = require('@mailsac/cypress/node');
module.exports = defineConfig({
video: false,
screenshotOnRunFailure: false,
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {
on('task', createMailsacTasks());
return config;
},
},
});
Then add the command registration to cypress/support/e2e.js or cypress/support/e2e.ts:
import '@mailsac/cypress/commands';
The command passes message criteria to a Node task; only the resulting message data comes back to the test. Automatic failure screenshots and video are disabled here because password-reset tokens appear in URLs. Also avoid logging message bodies, links, or codes.
Wait for the right email and follow its reset link
Save this spec as cypress/e2e/password-reset.cy.js. It uses the routes, button labels, and subject from the example app. When applying it to your own app, change the inbox, subject, reset path, selectors, and assertions to match your implementation. Start your app before running Cypress and configure it to send to the chosen test inbox.
import { extractLink } from '@mailsac/cypress';
it('resets a password and signs in', () => {
const email = 'your-private-test-inbox@mailsac.com';
const newPassword = 'my-new-demo-password';
let receivedAfter;
cy.visit('/forgot-password');
cy.get('input[name=email]').type(email);
// Capture immediately before requesting the email.
cy.then(() => { receivedAfter = new Date().toISOString(); });
cy.contains('button', 'Send reset email').click();
cy.then(() => cy.mailsacWaitForMessage({
email,
receivedAfter,
subject: 'Reset your Example App password',
timeoutMs: 60_000,
pollIntervalMs: 1_000,
})).then((message) => {
const resetUrl = extractLink(message, {
origin: new URL(Cypress.config('baseUrl')).origin,
pathname: '/reset',
});
cy.visit(resetUrl, { log: false });
});
cy.get('input[name=password]').type(newPassword, { log: false });
cy.contains('button', 'Update password').click();
cy.contains('h1', 'Password updated').should('be.visible');
cy.contains('a', 'Sign in').click();
cy.get('input[name=email]').type(email);
cy.get('input[name=password]').type(newPassword, { log: false });
cy.contains('button', 'Sign in').click();
cy.get('[role=status]').should('contain', 'signed in successfully');
});
With your app running and the test adapted to it, run:
npx cypress run --spec cypress/e2e/password-reset.cy.js
receivedAfter excludes older mail. Capturing it before the click also prevents a fast-arriving message from being excluded. An exact subject narrows the result; you can additionally match a sender with from, or use subjectIncludes for a subject substring. All supplied filters must match.
extractLink requires one distinct link with your app’s exact origin and reset path. It fails if the email has no matching link or several different matching links. That keeps a test from accidentally following a footer, help page, or unrelated website. If your sender wraps links in a tracking URL, disable tracking for these test messages or explicitly test that redirect flow.
Keep email tests reliable in CI
- Bound the wait. The default is 30 seconds; the example allows 60. The maximum is 120 seconds, including requests and retries. A timeout should fail the test rather than wait indefinitely.
- Separate parallel tests. Use a distinct inbox or a unique subject per test. A timestamp alone cannot distinguish simultaneous requests to one inbox. Keep the runner’s clock synchronized.
- Use dedicated inboxes. Each poll checks the latest 100 messages. Avoid a busy shared catch-all. Polls and message reads consume Mailsac API operations; choose your interval and concurrency accordingly.
- Protect reset links. Use a private inbox or owned domain for sensitive messages. The integration reads mail; it does not delete messages or change retention settings.
To run the cloned repository’s fixture example in GitHub Actions, no service secrets are needed:
name: Email test example
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:e2e
For your own app’s live-delivery CI test, start the app and its mail sender, provide MAILSAC_API_KEY through the CI secret store, and run your adapted Cypress spec. Keep that explicit live test separate from the local fixture run.
Testing verification codes too
For an OTP flow, use extractCode(message) on the matching message to return one distinct standalone six-digit code, then enter it with { log: false }. The helper rejects missing or ambiguous codes and supports custom patterns. See the OTP example and API reference.
Cypress email testing FAQ
Can Cypress read emails?
Not on its own: Cypress drives the browser, and email arrives outside it. A Cypress task running in Node can call an inbox API, though. That’s what @mailsac/cypress does: cy.mailsacWaitForMessage() polls Mailsac from Node until the matching message arrives and returns its content to your test.
How do I test email OTP or verification codes in Cypress?
Wait for the message as in the reset example, then call extractCode(message) and type the result with { log: false }. It returns one distinct six-digit code by default and fails if the code is missing or ambiguous; you can pass a custom pattern for other formats.
Do I need my own email server or domain?
No. Any @mailsac.com address can receive mail without setup, which is fine for made-up test accounts. Public inboxes are readable by anyone, so for reset links and codes on realistic accounts use a private address or a custom domain. Mailsac can host a test subdomain for you, with no DNS changes needed.
Will this work in CI with parallel runs?
Yes. Give each test its own address (or a unique subject), capture receivedAfter just before triggering the email, and supply MAILSAC_API_KEY from your CI secret store. Each poll and message read counts as an API operation, so set the poll interval to match your plan.
The original 2023 video remains available for historical reference. It predates this integration; use the package and code above for the current workflow.
Create your Mailsac account, install the Cypress integration, or explore the complete password-reset example.