Das Problem mit statischen Test-Postfächern
Playwright-Tests gegen echte Signup-Flows scheitern oft an einem trivialen Punkt: Du brauchst eine E-Mail-Adresse, die existiert, Mails empfängt und den Posteingang deines Teams nicht verschmutzt. Statische Test-Accounts (qa@deinedomain.com) werden mit der Zeit zur Müllhalde — Onboarding-Mails, Passwort-Resets und Welcome-Sequenzen stapeln sich, bis jemand manuell aufräumt. Parallele CI-Runs kollidieren in derselben Inbox und schnappen sich gegenseitig die Verifizierungsmails.
Die gängigen Workarounds (Mailpit, MailHog, MailCrab) sind gut für lokale Entwicklung, aber für Cloud-CI umständlich: Du musst sie als Service betreiben, irgendwie exponieren und deine App so verdrahten, dass sie statt an echtes SMTP an diese Tools sendet. Machbar, aber nicht derselbe Code-Pfad, den du ausrollst.
Die Mailiy-API in drei Calls
Die Mailiy-REST-API ist genau dafür gemacht. In einem beforeEach-Hook holst du dir per POST /v1/mailbox eine frische Adresse, optional mit ttlMinutes:
import { test } from '@playwright/test'
test.beforeEach(async ({ context }) => {
const res = await fetch('https://api.mailiy.com/v1/mailbox', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MAILIY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ttlMinutes: 30 }),
})
const { id, address } = await res.json()
context.mailboxId = id
context.address = address
})
API-Keys beginnen mit ml_pk_ und werden in den Account-Settings erzeugt. Lege den Key in deinen CI-Secret-Store, nicht ins Repo.
Im Test pollst du dann GET /v1/mailbox/:id/messages, bis die Verifizierungsmail ankommt:
async function waitForMail(mailboxId: string, timeoutMs = 30_000) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const r = await fetch(`https://api.mailiy.com/v1/mailbox/${mailboxId}/messages`, {
headers: { 'Authorization': `Bearer ${process.env.MAILIY_API_KEY}` },
})
const { messages } = await r.json()
if (messages.length > 0) return messages[0]
await new Promise(r => setTimeout(r, 1000))
}
throw new Error('Verifizierungsmail kam nicht rechtzeitig')
}
Ein Intervall von einer Sekunde mit 30 Sekunden Timeout reicht — das Backend antwortet in der Regel in unter einer Sekunde, weil SMTP-Ingest und API-Read gegen dieselbe Postgres-Instanz laufen.
Optional: Webhooks statt Polling
Wenn du lieber synchron arbeitest, registrierst du per POST /v1/webhooks mit { mailboxId, url } einen Push auf einen lokalen ngrok-Tunnel:
await fetch('https://api.mailiy.com/v1/webhooks', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.MAILIY_API_KEY}` },
body: JSON.stringify({
mailboxId: context.mailboxId,
url: `${process.env.NGROK_URL}/inbound`,
}),
})
Für CI ist Polling meist einfacher — du musst nichts inbound exponieren.
Teardown
Nach dem Test kannst du entweder die TTL die Arbeit machen lassen oder explizit löschen:
test.afterEach(async ({ context }) => {
await fetch(`https://api.mailiy.com/v1/mailbox/${context.mailboxId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${process.env.MAILIY_API_KEY}` },
})
})
Explizites Löschen ist freundlich zu deinem Rate-Limit-Budget und macht Test-Runs hermetisch. Die TTL drüber laufen zu lassen, ist für Suiten mit geringem Volumen in Ordnung.
Rate-Limit-Budget
Dein API-Tier bestimmt sowohl die Monats-Quota als auch das Per-Minute-Limit:
| Tier | Calls/Monat | Rate |
|---|---|---|
| api_free | 500 | 10 req/min |
| api_lite | 10.000 | 60 req/min |
| api_pro | 100.000 | 300 req/min |
Eine typische PR-Pipeline mit 50 Tests verbraucht pro Test 3–5 Calls (Create, Poll, optional Delete) — bei 100 PRs pro Tag passt das komfortabel in den Pro-Plan. Für ein kleines Team mit leichter CI-Nutzung ist auch der Free-Tarif echt brauchbar.
Wenn du mehr Per-Minute-Durchsatz brauchst als Pro liefert (300 req/min), geht Business auf 1.000 req/min, Enterprise ist unbegrenzt.
Was du dadurch bekommst
- Keine Kollisionen mehr in geteilten Postfächern bei parallelen CI-Runs
- Hermetische Test-Daten — jeder Test startet mit frischer Inbox
- Echter SMTP-Pfad — du triffst deinen echten Signup-Endpunkt, nicht einen Mock
- Sub-Sekunden-Feedback — Polling holt Nachrichten zuverlässig innerhalb von ~1s nach SMTP-Eingang
Für ein vollständiges Beispiel mit Cypress (statt Playwright), siehe die Beispiele in der API-Doku.
Jetzt ausprobieren → Wegwerf-Adresse auf der mailiy-Startseite generieren — ein Klick, in unter zwei Sekunden bereit, keine Anmeldung, kein Account.