Test automation & AI agents
Read the email your app sends — from your CI tests, or from an AI agent — without a human opening an inbox.
How it works
- Point your app's SMTP settings at a MailWarren mailbox (the credentials are on the mailbox's page in a Warren). Everything it sends is captured instead of delivered.
- Create an API key for that mailbox — see API keys.
- In your test, trigger the action that sends the email (sign up, reset a password, place an order), then wait for the email and read its verification code or link.
Waiting is a long-poll: the request is held open until a matching email arrives, so there is no sleep(5000) in your tests.
Only mail received after your test started counts, so an email left over from an earlier run can never make a test pass by accident.
API keys
Two kinds of key can read an inbox. Both go in an Authorization: Bearer … header.
| Key | Sees | Where to get it |
|---|---|---|
Mailbox keymw_sandbox_… |
One mailbox. The best choice for a test suite or one agent — it can't touch anything else. The same key can also put email into that mailbox (POST /v1/sandbox/send). |
Open the mailbox in a Warren → Sandbox API keys |
Account keymw_mcp_… |
Every mailbox your account (or organisation) owns. Pass mailbox_id to say which. Made for AI agents and tools that work across projects. |
Account → MCP keys (organisation Owners and Admins) |
A key only ever sees its own owner's mail — asking for anyone else's returns "not found". Revoke a key from the same page it was created on.
JavaScript / TypeScript SDK
A small, dependency-free client for Node 18+ (Playwright, Cypress, Jest, Vitest, plain scripts). Written so a test reads like the flow it checks.
# single file, no dependencies (Node 18+)
curl -O http://mailwarren.io/downloads/mailwarren-test.js
curl -O http://mailwarren.io/downloads/mailwarren-test.d.ts # optional: TypeScript types
Then const { MailWarren } = require('./mailwarren-test').
Configure it with two environment variables (or pass apiKey / baseUrl to the constructor):
MAILWARREN_API_KEY=mw_sandbox_... # a mailbox key (or an account key, mw_mcp_...)
MAILWARREN_API_URL=https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1
Playwright
import { test, expect } from '@playwright/test';
import { MailWarren } from '@mailwarren/test'; // or: require('./mailwarren-test')
const mw = new MailWarren(); // reads MAILWARREN_API_KEY and MAILWARREN_API_URL
test.beforeEach(async () => { await mw.clear(); }); // an empty inbox, and only new mail counts
test('sign up and verify by email', async ({ page }) => {
const address = `alice+${Date.now()}@test.dev`;
await page.goto('https://staging.example.com/signup');
await page.fill('#email', address);
await page.click('text=Create account');
const email = await mw.waitForEmail({ to: address }); // waits up to 30 s
await page.fill('#code', email.code!); // e.g. "482913"
await page.click('text=Verify');
await expect(page.locator('h1')).toHaveText('Welcome');
});
test('password reset link works', async ({ page }) => {
await page.goto('https://staging.example.com/forgot');
await page.fill('#email', 'alice@test.dev');
await page.click('text=Send reset link');
const email = await mw.waitForEmail({ to: 'alice@test.dev', subject: 'Reset' });
await page.goto(email.findLink('reset')!); // the first link that looks like a reset link
await expect(page.locator('#new-password')).toBeVisible();
});
Cypress
Cypress runs your spec in the browser, so call the SDK from a Node task in cypress.config.js:
// cypress.config.js
const { defineConfig } = require('cypress');
const { MailWarren } = require('@mailwarren/test'); // or: require('./mailwarren-test')
module.exports = defineConfig({
e2e: {
setupNodeEvents(on) {
const mw = new MailWarren();
on('task', {
clearInbox: () => mw.clear(),
async waitForEmail(filters) {
const email = await mw.waitForEmail(filters);
return { code: email.code, subject: email.subject, verifyLink: email.findLink('verify') };
},
});
},
},
});
// cypress/e2e/signup.cy.js
it('verifies a new account by email', () => {
cy.task('clearInbox');
cy.visit('/signup');
cy.get('#email').type('alice+cy1@test.dev');
cy.contains('Create account').click();
cy.task('waitForEmail', { to: 'alice+cy1@test.dev' }).then(({ code }) => {
cy.get('#code').type(code);
});
});
Jest / Vitest
const { MailWarren } = require('@mailwarren/test'); // or: require('./mailwarren-test')
const mw = new MailWarren();
beforeEach(() => mw.clear());
test('welcome email has the right content and no template leftovers', async () => {
await createAccount('alice+j1@test.dev'); // your code under test
const email = await mw.waitForEmail({ to: 'alice+j1@test.dev', subject: 'Welcome' });
expect(email.from).toBe('hello@example.com');
expect(email.findLink('verify')).toMatch(/^https:\/\/example\.com\/verify/);
const report = await email.analyze(); // lint: {{placeholders}}, alt text, http links...
expect(report.errors).toBe(0);
});
test('unsubscribed users get nothing', async () => {
await sendNewsletter();
await mw.expectNoEmail({ to: 'unsubscribed@test.dev', within: 3000 });
});
What you get
waitForEmail({ to, from, subject, text, since, timeout })— resolves with the newest matching email or throws aMailWarrenTimeoutErrorthat names what it was waiting for.expectNoEmail({ to, within })— for "must not send" checks.clear()— empties the mailbox and makes "from now on" the defaultsince. Put it inbeforeEach.mark()does the second half without deleting anything.list(),get(id),mailboxes(),send({ to, subject, text, html }).- On an email:
code,codes,links,findLink('verify' | 'reset' | /regex/),subject,from,to,text,html,attachment(indexOrName | "cid:logo"),analyze()andassertQuality()(see Pre-send checks).
Python SDK
The same client for pytest, unittest, Django and Flask test suites: one file, standard library only, Python 3.8+. It has the same behaviour as the JavaScript SDK —
long-polling waits, "only new mail counts", codes and links pulled out for you, the pre-send checks — in Python spelling (wait_for_email, find_link, assert_quality).
# single file, no dependencies (Python 3.8+) -- put it next to your tests
curl -O http://mailwarren.io/downloads/mailwarren_test.py
Then from mailwarren_test import MailWarren.
It reads the same two environment variables as above (MAILWARREN_API_KEY, MAILWARREN_API_URL), or take api_key= / base_url= in the constructor.
pytest
# conftest.py
import pytest
from mailwarren_test import MailWarren # reads MAILWARREN_API_KEY and MAILWARREN_API_URL
@pytest.fixture
def mailbox():
mw = MailWarren()
mw.clear() # an empty inbox, and only new mail counts
return mw
# test_signup.py
def test_signup_sends_a_verification_code(client, mailbox):
client.post("/signup", {"email": "alice+t1@test.dev"}) # your app sends the email
email = mailbox.wait_for_email(to="alice+t1@test.dev") # waits up to 30 s
assert email.code and len(email.code) == 6 # e.g. "482913"
client.get(email.find_link("verify")) # the first link that looks like a verify link
def test_password_reset_link(client, mailbox):
client.post("/forgot", {"email": "alice@test.dev"})
email = mailbox.wait_for_email(to="alice@test.dev", subject="Reset")
assert client.get(email.find_link("reset")).status_code == 200
def test_welcome_email_quality(client, mailbox):
client.post("/signup", {"email": "alice+t2@test.dev"})
mailbox.wait_for_email(to="alice+t2@test.dev").assert_quality() # fails listing every problem
def test_unsubscribed_users_get_nothing(mailbox):
send_newsletter()
mailbox.expect_no_email(to="unsubscribed@test.dev", within=3)
unittest / Django
# Django / unittest / anything else
import unittest
from mailwarren_test import MailWarren
class SignupEmailTests(unittest.TestCase):
def setUp(self):
self.mail = MailWarren()
self.mail.clear()
def test_verification_code(self):
self.client.post("/signup/", {"email": "alice+u1@test.dev"})
email = self.mail.wait_for_email(to="alice+u1@test.dev", timeout=20)
self.assertRegex(email.code, r"^\d{6}$")
What you get
wait_for_email(to=, from_=, subject=, text=, since=, timeout=30)— returns the newest matching email or raises aMailWarrenTimeoutErrorthat names what it was waiting for. (from_has an underscore becausefromis a Python keyword.)expect_no_email(to=, within=3)— for "must not send" checks.clear()empties the mailbox and makes "from now on" the defaultsince;mark()does the second half without deleting anything.list(),get(id),mailboxes(),send(to, subject, text=).sincetakes adatetime, atimedelta(an age), an ISO time or an age string like"5m".- On an email:
code,codes,links,find_link("verify" | "reset" | re.compile(...)),subject,from_,to,text,html,attachment(index_or_name | "cid:logo"),analyze()andassert_quality()(see Pre-send checks). - Errors are
MailWarrenErrorwithstatus,code,planandretry_after_seconds; a per-minute 429 is retried for you (see Plans & limits).
.NET SDK (C#)
For xUnit, NUnit, MSTest, Playwright for .NET and SpecFlow: one file, no dependencies beyond HttpClient and System.Text.Json, .NET 6 or later.
Same behaviour as the other SDKs, with async methods and typed results (WaitForEmailAsync, FindLink, AssertQualityAsync).
# single file, no dependencies (.NET 6+) -- add it to your test project
curl -o MailWarrenClient.cs http://mailwarren.io/downloads/MailWarrenTest.cs
Then using MailWarren.Test;.
It reads the same two environment variables (MAILWARREN_API_KEY, MAILWARREN_API_URL), or pass a MailWarrenOptions with ApiKey / BaseUrl.
xUnit
using MailWarren.Test;
using Xunit;
public class SignupTests : IAsyncLifetime
{
private readonly MailWarrenClient _mail = new(); // reads MAILWARREN_API_KEY and MAILWARREN_API_URL
public async Task InitializeAsync() => await _mail.ClearAsync(); // an empty inbox, and only new mail counts
public Task DisposeAsync() { _mail.Dispose(); return Task.CompletedTask; }
[Fact]
public async Task Signup_sends_a_verification_code()
{
await _app.PostAsync("/signup", Json(new { email = "alice+t1@test.dev" })); // your app sends the email
var email = await _mail.WaitForEmailAsync(new WaitOptions { To = "alice+t1@test.dev" }); // waits up to 30 s
Assert.Matches(@"^\d{6}$", email.Code); // e.g. "482913"
await _app.GetAsync(email.FindLink("verify")!); // the first link that looks like a verify link
}
[Fact]
public async Task Welcome_email_passes_the_quality_bar()
{
await _app.PostAsync("/signup", Json(new { email = "alice+t2@test.dev" }));
var email = await _mail.WaitForEmailAsync(new WaitOptions { To = "alice+t2@test.dev", Subject = "Welcome" });
await email.AssertQualityAsync(); // throws listing every problem: no errors, spam score <= 5
}
[Fact]
public async Task Unsubscribed_users_get_nothing()
{
await SendNewsletterAsync();
await _mail.ExpectNoEmailAsync(new WaitOptions { To = "unsubscribed@test.dev", Timeout = TimeSpan.FromSeconds(3) });
}
}
What you get
WaitForEmailAsync(new WaitOptions { To, From, Subject, Text, Since, SinceAge, Timeout })— returns the newest matchingEmailor throws aMailWarrenTimeoutExceptionthat names what it was waiting for.ExpectNoEmailAsync(...)— for "must not send" checks.ClearAsync()empties the mailbox and makes "from now on" the defaultSince;Mark()does the second half without deleting anything.ListAsync(),GetAsync(id),MailboxesAsync(),SendAsync(to, subject, text: ...).- On an email:
Code,Codes,Links,FindLink("verify" | "reset" | new Regex(...)),Subject,From,To,Text,Html,GetAttachmentAsync(indexOrName | "cid:logo"),AnalyzeAsync()andAssertQualityAsync()(see Pre-send checks) — the report comes back as typed records. - Errors are
MailWarrenExceptionwithStatus,Code,PlanandRetryAfterSeconds; a per-minute 429 is retried for you (see Plans & limits).
REST API
Anything that can make an HTTPS call can use it — the SDKs are only a convenience, and Go, Ruby, PHP, Java or a shell script can use the API directly. Base URL for this deployment:
https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1
BASE=https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1
KEY=mw_sandbox_...
# wait up to 20 s for an email to alice+t1@test.dev that arrived in the last 2 minutes
curl -s -H "Authorization: Bearer $KEY" \
"$BASE/inbox/wait?to=alice%2Bt1@test.dev&since=2m&timeout=20"
# list what is in the mailbox
curl -s -H "Authorization: Bearer $KEY" "$BASE/inbox/messages?limit=10"
# one message in full, with the lint report
curl -s -H "Authorization: Bearer $KEY" "$BASE/inbox/messages/<id>?analyze=true"
# empty the mailbox between tests
curl -s -X DELETE -H "Authorization: Bearer $KEY" "$BASE/inbox/messages"
| Request | What it does |
|---|---|
GET /inbox/mailboxes | The mailboxes this key can read. |
GET /inbox/messages | List or search, newest first. Filters: mailbox_id, to, from, subject, text (also searches bodies), since, limit (default 20, max 200). Text filters are case-insensitive "contains". |
GET /inbox/messages/{id} | One message in full, with code, codes and links. Each attachment has contentId (what the HTML refers to as cid:..., or null) and disposition (inline for an embedded image, otherwise attachment). Add ?analyze=true for the lint report. |
GET /inbox/wait | Long-poll. Same filters, plus timeout in seconds (default 20, max 25). Returns the newest match in full. |
DELETE /inbox/messages | Delete every message in a mailbox. A mailbox key needs no mailbox_id; an account key must name one. |
GET /inbox/attachments/{id} | An attachment's bytes as base64 in JSON. |
The wait response
{
"matched": true,
"message": {
"id": "…", "mailboxId": "…",
"fromAddress": "noreply@app.example.com",
"toAddresses": ["alice+t1@test.dev"],
"subject": "Verify your account",
"receivedAtUtc": "2026-09-21T09:48:48Z",
"bodyText": "Your verification code is 482913 …",
"bodyHtml": "<p>…</p>",
"attachments": [{ "id": "…", "fileName": "invoice.pdf", "contentType": "application/pdf", "sizeBytes": 30, "contentId": null, "disposition": "attachment" }],
"code": "482913",
"codes": [{ "value": "482913", "confidence": "high", "context": "Your verification code is 482913. It expires in 10 m" }],
"links": [{ "url": "https://app.example.com/verify?token=abc123", "text": "Confirm my email", "purpose": "verify" }]
}
}
// no email yet: { "matched": false, "timedOut": true, "waitedSeconds": 20, "since": "…" }
Good to know
sinceis an ISO-8601 time or an age such as30s,5m,2h,1d. Wait defaults to the last 10 minutes; the SDK defaults to "since the client was created or last cleared". An age avoids any clock difference between your machine and ours.- One request holds for at most 25 seconds. If nothing has arrived it answers
matched: false; ask again. The SDK does this for you up to your timeout. - Codes and links are found by heuristics. A code is 4–8 digits (or
123 456, or 6–8 upper-case letters and digits) —codeis only set when a word like code, OTP, PIN, passcode, token, verification sits just before it, so an order number is never mistaken for one; every candidate is incodeswith a confidence. A link'spurpose(verify, reset, invite, login, unsubscribe, other) is guessed from its URL and text. - The lint reports what is in the message — empty body, unrendered
{{placeholders}}, images without alt text,http://links, no plain-text part, oversized HTML — not how any mail client will render it. - Errors are JSON:
{ "error": "…" }. A bad or revoked key is401; something that isn't yours is404.
Pre-send checks
Before real people get an email, MailWarren can tell you what's wrong with it. Every captured email has a Checks tab in the app, and the same report is available to tests, scripts and AI agents. It looks at:
| What | Examples of what it finds |
|---|---|
| Content | An empty body or subject, template variables that were never filled in ({{name}}, undefined), a very long subject. |
| Spam risk | A score on SpamAssassin's scale (5 is the usual junk threshold) with the rules that added to it: all-caps subjects, junk-mail phrases, URL shorteners, links to bare IP addresses, link text that shows one address but goes to another, HTML with no plain-text part, mostly-image emails. |
| Mail clients | How it will hold up in Gmail, Outlook for Windows, Apple Mail and Yahoo Mail: flexbox and grid, positioning, background images, CSS variables, external stylesheets, scripts, forms, SVG, video, web fonts, Gmail's ~102 KB clipping and ~16 KB style limit. |
| Accessibility | Images without alt text, no lang, "click here" links, low text contrast (WCAG 4.5:1), tiny text, layout tables without role="presentation". |
| Phones & dark mode | No viewport tag, fixed widths wider than a phone with no responsive rules, and whether the email declares dark-mode support. |
| Links | Empty, malformed, javascript:, placeholder or local-address links, http:// links and shorteners — and, on request, whether each link actually loads. |
In the app
Open any message and choose Checks: a spam meter, a status for each of the four mail clients, every finding grouped by category, and a Check links now button. The Preview tab also has a Mobile width and a simulated Dark mode.
In your tests
const email = await mw.waitForEmail({ to: 'alice+t1@test.dev', subject: 'Welcome' });
// fail the build if the welcome email regresses: any error, or a spam score above 5
await email.assertQuality();
// stricter: warnings fail too (except ones you've decided to accept), and every link must load
await email.assertQuality({ failOn: 'warning', ignore: ['no-unsubscribe'], links: 'live', maxSpamScore: 3 });
// or read the report yourself
const report = await email.analyze();
console.log(report.spam.score, report.spam.verdict); // 1.6 'low-risk'
console.log(report.compatibility); // [{ client: 'Gmail', status: 'ok', problems: [] }, ...]
console.log(report.issues.filter((i) => i.category === 'accessibility'));
Over REST it's the same message endpoint with a flag; the reply gains an analysis object:
# static checks (no network): content, spam risk, mail-client compatibility, accessibility, phones, dark mode, link addresses
curl -s -H "Authorization: Bearer $KEY" "$BASE/inbox/messages/<id>?analyze=true"
# ...plus a live request to each link (slower, up to ~18 s)
curl -s -H "Authorization: Bearer $KEY" "$BASE/inbox/messages/<id>?links=live"
"analysis": {
"errors": 1, "warnings": 4, "infos": 3,
"issues": [
{ "id": "unrendered-placeholder", "category": "content", "severity": "error",
"message": "Looks like a template variable was not filled in: {{name}}", "clients": null },
{ "id": "compat-flexbox", "category": "compatibility", "severity": "warning",
"message": "Flexbox (display:flex) is ignored by Outlook for Windows …", "clients": ["Outlook (Windows)"] }
],
"spam": { "score": 2.6, "threshold": 5, "verdict": "borderline",
"rules": [{ "id": "HTML_ONLY", "points": 0.8, "description": "HTML-only: no plain-text part." }] },
"compatibility": [
{ "client": "Gmail", "status": "ok", "problems": [] },
{ "client": "Outlook (Windows)", "status": "issues", "problems": ["Flexbox …"] },
{ "client": "Apple Mail", "status": "ok", "problems": [] },
{ "client": "Yahoo Mail", "status": "ok", "problems": [] }
],
"links": null // filled in (results per link) only with links=live
}
category is one of content, deliverability, compatibility, accessibility, mobile or links;
severity is error, warning or info. AI agents get the same report from the analyze_message tool (check_links=true for the live link check).
The live link check
links=live (or Check links now) sends a request to each link — up to 15 — and reports whether it loads, redirects or is broken, and reports a domain that doesn't exist. It is deliberately careful:
- It never requests a private or internal address —
localhost,10.x,192.168.x, cloud metadata addresses, or a hostname that resolves to one — and only ports 80, 443, 8080 and 8443. Those links are reported as "not requested". - It never requests a link that looks single-use (verify, reset, invite, login, unsubscribe): opening one could use up its token or unsubscribe someone. Those are reported as "skipped".
- It sends only
HEAD(or a one-byte range request), reads no page content, follows at most three redirects, and identifies itself asMailWarren-LinkCheck. - Some sites refuse automated checks (HTTP 403/429); those are reported as "couldn't verify", not broken.
What it can't tell you
- It doesn't render the email in Gmail or Outlook. Client findings come from published support data (such as caniemail.com); clients change, so test anything critical in the real client.
- The spam score is an estimate from the message's content. Whether real inboxes accept your mail depends far more on your sending domain's reputation, SPF, DKIM and DMARC, and your list — none of which a captured message shows. A low score is not a guarantee.
- The dark-mode preview is a simulation: it shows the email's own dark styles if it has them, otherwise an inverted approximation — good for spotting a dark logo on a transparent background, not a picture of any one client.
AI agents (MCP)
MailWarren speaks the Model Context Protocol, so a coding agent can send, read and check email itself — "did the signup email go out, and is it right?" — without you copying anything between windows. It uses the same keys and the same search as the REST API.
Connect
Use an account key, or a mailbox key to pin the agent to one mailbox. In Claude Code:
claude mcp add --transport http mailwarren https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/mcp \
--header "Authorization: Bearer mw_mcp_..."
Or in any client that reads an MCP config (Cursor, Claude Desktop with a remote-server connector, and others):
{
"mcpServers": {
"mailwarren": {
"type": "http",
"url": "https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/mcp",
"headers": { "Authorization": "Bearer mw_mcp_..." }
}
}
}
The server URL is https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/mcp. Nothing is stored between calls and there is nothing to install or run.
Tools
| Tool | What the agent uses it for | |
|---|---|---|
list_mailboxes, list_warrens | Find the right mailbox. | read-only |
list_messages | See what a mailbox holds; filter by recipient, sender, subject, text or time. | read-only |
wait_for_message | Wait up to 25 s for an email to arrive, then get it with its code and links. | read-only |
get_message | Read one email in full. | read-only |
analyze_message | Run the pre-send checks on one email — content, spam risk, Gmail/Outlook/Apple Mail/Yahoo compatibility, accessibility, phones and links (check_links=true also requests each link, where the plan includes it) — and explain what is wrong. | read-only |
send_test_email | Put an example email into a mailbox to try a flow. Counts against the monthly capture allowance; never sends real mail. | writes |
delete_all_messages | Empty a mailbox between runs. | destructive |
Each tool carries MCP safety annotations, so a client can auto-approve the read-only ones and ask before running the destructive one. The server also offers three ready-made prompts — verify a signup email, review an email's quality and find a missing email — and tells the agent, on connect, how the tools fit together.
What it looks like
You: I just ran the signup flow on staging with alice+demo1@test.dev. Did the welcome email go out correctly?
Agent: (wait_for_message to=alice+demo1@test.dev since=5m)
→ matched: "Welcome to Example", code 482913, links: verify, unsubscribe
(analyze_message)
→ 1 error: unrendered template variable {{first_name}} in the greeting
2 warnings: HTML-only (no plain-text part), 1 of 3 images has no alt text
The email arrived, but the greeting reads "Hi {{first_name}}," — your template variable
wasn't filled in. The verify link is https://staging.example.com/verify?token=… .
Plans & limits
The test API, the SDK, the MCP server and the spam, mail-client, accessibility and link-address checks are on every plan, including Free. What a paid plan adds is headroom: a higher request rate, the live link check, and the plan's larger capture allowance and longer retention.
| Plan | Price | Test API, SDK & MCP requests | Live link checks |
|---|---|---|---|
| Free | $0 | 30 a minute | Not included |
| Starter | $15/mo | 90 a minute | 25 a day |
| Team | $40/mo | 240 a minute | 150 a day |
| Business | $120/mo | 600 a minute | 600 a day |
Larger plans and Enterprise: contact us. See Plans in your account for captured messages, seats and retention.
- Requests are counted per account, across every key it has (mailbox keys, account keys, the MCP server), not per key or per IP address — a second key doesn't add capacity.
Go over the limit and the API answers
429with aRetry-Afterheader and"code": "rate_limited"; the SDKs wait and retry for you (up to three times, at most a minute each), and an MCP agent is told how many seconds to wait. A long-poll (waitForEmail,wait_for_message) counts as one request however long it is held. - Live link checks are counted per account per UTC day and reset at 00:00 UTC; one check requests every link in one email. A plan without them answers
403with"code": "plan_limit"; a used-up daily allowance answers429with"code": "quota_exceeded"(the SDKs do not retry this — waiting would not help). The static checks keep working either way. - Captured mail counts toward your plan's monthly capture allowance and is kept for your plan's retention period — the same as mail captured over SMTP. Reading, waiting and deleting cost nothing.
- An account key can read every mailbox you own; keep it out of client-side code and shared logs. Prefer a mailbox key wherever one mailbox is enough.
- Do not point real customer email at MailWarren — see the Acceptable Use Policy.
Questions or something missing? Write to support@mailwarren.io.