Testing email in Cypress without signup
Cypress cannot read a mailbox, but it can call an HTTP API. Email Blackhole gives your app a dummy SMTP server to send to and gives your test one call, wait, that returns the email as soon as it arrives. There is no account to create and no API key to request.
1. Get the settings
Open the sandbox, press Copy as .env and paste the result into your project's .env (git-ignored). Your app uses the SMTP_* values; Cypress uses EBH_URL, EBH_ID and EBH_SECRET. A free session is for your own machine and ends after 60 minutes without use: when it expires, press Copy as .env again and replace the block in .env. A pipeline cannot do that (creating a session needs a person at a browser), so CI needs a permanent inbox, a paid plan whose values never expire, stored as CI secrets.
2. Hand the variables to Cypress
cypress.config.js:
import { defineConfig } from 'cypress'
import 'dotenv/config'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
env: {
EBH_URL: process.env.EBH_URL,
EBH_ID: process.env.EBH_ID,
EBH_SECRET: process.env.EBH_SECRET,
},
},
})
Your app itself must send through the SMTP_* settings (any recipient is accepted, nothing is delivered).
3. Custom commands
cypress/support/commands.js:
const inbox = () => ({
url: `${Cypress.env('EBH_URL')}/api/sessions/${Cypress.env('EBH_ID')}`,
headers: { Authorization: `Bearer ${Cypress.env('EBH_SECRET')}` },
})
// The inbox position right now. Take it BEFORE the action that sends the email.
Cypress.Commands.add('emailMark', () =>
cy.request({ url: `${inbox().url}/messages`, headers: inbox().headers }).then(({ body }) =>
Math.max(0, ...body.messages.map((m) => m.seq)),
),
)
// Waits (up to 60 s) for a message newer than `after`, sent to `to`, whose subject contains `subject`.
Cypress.Commands.add('waitForEmail', (after, to, subject = '') => {
const q = new URLSearchParams({ after: String(after), to, subject, timeout: '60' })
return cy
.request({ url: `${inbox().url}/wait?${q}`, headers: inbox().headers, timeout: 70000 })
.then((res) => {
expect(res.status, 'the email should arrive').to.eq(200)
return res.body.messages[0] // { subject, text, html, links, codes, ... }
})
})
4. The test
it('confirms a new account from the email', () => {
const email = `user-${Date.now()}@example.com`
cy.emailMark().then((after) => {
cy.visit('/signup')
cy.get('input[type=email]').type(email)
cy.contains('button', 'Create account').click()
cy.waitForEmail(after, email, 'confirm').then((message) => {
cy.visit(message.links[0]) // or message.codes[0] for a one-time code
})
})
cy.contains('Your email is confirmed')
})
The wait only considers mail newer than the mark, so a leftover email from an earlier run cannot satisfy it, and there is no cy.wait(5000).
Or use the ready-made helper
The emailblackhole npm package does the waiting for you inside Cypress tasks (so the secret never reaches the browser), and there is a runnable starter project:
- Package: npmjs.com/package/emailblackhole (source, MIT)
- Starter with a working forgot-password test and CI workflow: emailblackhole-cypress-starter (MIT)
Related: Playwright, Test email in CI, password-reset testing, API reference.