Email sandbox
Email quality checks
Every captured message has a Quality tab in the message viewer. It combines three checks, each with a summary badge at the top:
- Links: every link and image in the HTML and text parts is requested, to find broken links, timeouts, DNS failures and redirect chains, plus problems visible without a request, such as staging hosts.
- Accessibility: a WCAG-oriented review of the HTML, with a score out of 100.
- Authentication: SPF, DKIM, DMARC, ARC and BIMI results, explained in plain language with the DNS records to add or fix.
The HTML preview also has a color mode selector to see the message as dark-mode mail clients show it.
Plans
| Check | Free | Team | Business | Enterprise |
|---|---|---|---|---|
| Accessibility check, dark-mode preview | Yes | Yes | Yes | Yes |
| Link check, authentication results | No | Yes | Yes | Yes |
On a plan without them, the link and authentication endpoints answer 402 with { "code": "feature_unavailable", "limit": "qualityChecks", "upgradeUrl": "/billing" }. Any team member, and any API key (read-only ones included), can read the checks.
Link check
The link check runs in the background. The first request starts it:
curl -H "Authorization: Bearer $TESTLIE_API_KEY" https://app.testlie.com/api/messages/$MESSAGE_ID/links
# {"status":"running","startedAt":"2026-10-23T10:30:05.000Z"}Poll the same URL until it answers { "status": "done", "report", "cached" } or { "status": "failed", "error" }. The report is kept with the message; ?refresh=1 runs the check again (a refresh within 10 seconds of the last result returns that result). A check that stops answering is restarted after 90 seconds.
How URLs are requested:
- At most 50 unique http(s) URLs per message, 5 at a time, 5 seconds per URL in total.
HEADfirst, thenGETifHEADfails or answers 4xx or 5xx (many servers mishandleHEAD). Bodies are never downloaded.- Up to 5 redirects are followed and recorded, with the URL and status of each hop.
- Hosts that resolve to private, loopback or link-local addresses are not requested; they are reported as "Not checked". No cookies or credentials are sent.
Each URL in report.links has url, occurrences (where it appears: kind link or image, source html or text, the visible text and the line), checked, status, finalUrl, redirects, method, timeMs and problems (code, level, message):
| Code | Level | Meaning |
|---|---|---|
broken |
error | The final response is 4xx or 5xx |
timeout |
error | No response within 5 seconds |
dns |
error | The host does not resolve |
unreachable |
error | The connection was refused or reset |
too_many_redirects |
error | More than 5 redirects |
blocked |
warn | Not requested: a private or loopback target |
insecure |
warn | An http:// link, or an https:// link that redirects to http:// |
dev_host |
error | A development or staging host leaked into the email: localhost, private IPs, *.local, *.localhost, *.internal, *.test, staging., dev., qa., uat. and preview. subdomains, -staging names, non-standard ports |
mismatched_text |
warn | The visible link text is a URL or domain other than where the link goes (it looks like phishing) |
invalid_mailto |
error | A mailto: link without a valid address |
invalid_tel |
error / info | A tel: link with invalid characters or digit count (info: no + country code) |
missing_utm |
info | A campaign-looking message (with List-Unsubscribe, Precedence: bulk, or other UTM-tagged links) has a link without utm_source |
empty, relative, javascript, malformed |
error | An unusable link |
Accessibility
GET /api/messages/:id/accessibility answers at once with { report, cached }. The report is kept with the message; ?refresh=1 computes it again. It has a score (0 to 100), summary counts, stats (text characters, images, links, headings, tables, text per image) and findings grouped by rule, each with a title, level, help (how to fix it), count and up to 10 items (a snippet of the element, its line and a detail).
| Rule | Level | What it checks |
|---|---|---|
html-lang |
error | <html lang> is missing |
doc-title |
warn | <title> is missing or empty |
img-alt |
error | An <img> without alt |
img-decorative |
warn | Spacers, 1×1 pixels and other decorative images without alt="" |
img-alt-filename |
warn | An alt that is a file name (IMG_1234, hero.png) |
link-name |
error | A link with no text, image alt or aria-label |
link-vague |
warn | Link text such as "click here", "here", "read more" or "learn more" |
table-layout |
warn | A table without th, caption or thead, and without role="presentation" |
contrast |
error | Text below WCAG AA contrast: 4.5:1, or 3:1 for large text (24px, or 18.7px bold) |
font-small |
warn | Text below 14px |
heading-order |
warn | Skipped heading levels (h2 to h4) |
all-caps |
warn | Long passages in capitals, including text-transform: uppercase |
text-part |
warn | The message has no text/plain part |
image-ratio |
warn | Mostly images: under 100 characters of text, or under 50 per image |
Colors come from what email clients honour: inline color, background and background-color, <font color> and bgcolor, inherited from parent elements. Text over background images or gradients is skipped rather than guessed.
Authentication results
GET /api/messages/:id/auth works like the link check: it runs in the background, answers { "status": "running" } until the result is ready, and takes ?refresh=1. DNS queries time out after 3 seconds each and the whole check after 20 seconds; a timeout becomes a temperror result, not an error.
The report has:
| Field | Contents |
|---|---|
context |
The connecting client's ip, its helo name, the envelope mailFrom, the fromDomain, and privateIp |
overall |
pass, warn, fail or neutral, driven by DMARC |
spf, dkim, dmarc, arc, bimi |
One section each, with result (the raw keyword), verdict, summary (one plain sentence), details, fix and the record found in DNS. dkim lists each signature (domain, selector, algorithm, result, aligned); dmarc adds the policy and alignment; bimi the location |
authResults |
The Authentication-Results header a receiving server would add |
fix suggests the DNS records to publish: your SPF record with the sending IP added, the <selector>._domainkey record, or a starter _dmarc record.
Reading the results for captured mail. Messages reach Testlie from your application or relay, not from your production mail provider, so SPF is evaluated for whatever server connected to Testlie. An SPF fail may only mean that server is not in your SPF record, which matters only if it also sends your production mail. Messages sent from private addresses (CI, Docker) get a note saying SPF is not meaningful for them. DKIM and DMARC do not depend on the connecting server: they are the results to act on.
Dark-mode preview
The preview's color mode selector has three settings:
| Mode | What it does |
|---|---|
| Light | The message as sent |
| Dark | Applies the message's own @media (prefers-color-scheme: dark) styles, as Apple Mail and Outlook.com do |
| Dark (inverted) | Inverts a light design and re-inverts images, as the Gmail and Outlook apps do |
Over the API, add ?dark=respect or ?dark=invert to GET /api/messages/:id/html or GET /api/messages/:id/preview-url.
Related
- Spam reports: spam score, inbox placement and the Gmail/Yahoo bulk-sender checklist.
- Viewing messages: which HTML and CSS features each mail client supports.
- Email API: every message endpoint.