# Pre-Send Email HTML Compatibility Checker (`kingii98/email-html-compatibility-checker`) Actor

Check email HTML against 12 client profiles before you send it. Finds Outlook and Gmail breakages, dead or insecure assets, and Gmail clip-limit risk, with no browser and no render farm.

- **URL**: https://apify.com/kingii98/email-html-compatibility-checker.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 email analyzeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Pre-Send Email HTML Compatibility Checker

Check email HTML before you send it. The Actor finds design breaks in
Outlook and Gmail, and in ten other clients, without a browser and without a
render farm. Send it your email HTML. Get a client-by-client score, a list
of findings, and a send verdict.

### What it does

For each email, the Actor:

1. Reads the HTML you supply, either inline or from a public URL.
2. Tests the HTML and CSS against a static rule matrix for up to 12 email
   client profiles (Outlook desktop, Outlook.com, Gmail web/Android/iOS,
   Apple Mail macOS/iOS, Yahoo, AOL, Samsung Mail, Thunderbird, Orange.fr).
3. Sends one bounded HEAD request for each unique image and link that the
   HTML names, to check that the asset is reachable, served over HTTPS, and
   not oversize.
4. Reports structure flags: missing alt text, a missing preheader, table
   layout use, media query count, and the Gmail 102 KB clip limit.
5. Scores each client from 0 to 100 and gives one verdict: `SEND_OK`,
   `SEND_WITH_RISK`, or `DO_NOT_SEND`.

The Actor never renders the email and never opens a browser. It reads the
supplied HTML as text and sends network requests only to asset URLs that the
HTML itself names.

### Input

See [`.actor/input_schema.json`](.actor/input_schema.json) for the full
schema. The main fields:

| Field | Type | Default | Purpose |
| --- | --- | --- | --- |
| `emails` | array | the example email | Each item needs `name` and one of `html` or `htmlUrl`. Maximum 200 items, 2 MB of HTML each. If you leave this field empty, the Actor checks the example email from the default input. |
| `clientProfiles` | array of string | all 12 profiles | Which client profiles to score. |
| `checkRemoteAssets` | boolean | `true` | Send HEAD requests to check images and links. |
| `maxAssetsPerEmail` | integer | `60` | Cap on unique assets checked for each email. |
| `darkModeChecks` | boolean | `true` | Flag emails with no dark mode support signal. |
| `previousDatasetId` | string | — (optional) | Dataset ID from an earlier run. Marks each finding as new or already known. |

### Output

One dataset record for each email, plus one run-level summary record first.
Each email record holds:

- `emailName`, `htmlBytes`, `checkedAt`
- `clientScores`: one 0-100 score for each requested client profile
- `findings`: a list of `{severity, client, ruleId, cssPropertyOrTag, lineNumber, message, fixSnippet}`
- `assetFindings`: a list of `{url, kind, httpStatus, contentType, bytes, verdict}`
- `structureFlags`: `{missingAltCount, missingPreheader, tableLayoutUsed, mediaQueryCount, totalHtmlBytesOverGmailClipLimit}`
- `verdict`: `SEND_OK`, `SEND_WITH_RISK`, or `DO_NOT_SEND`
- `error`: set only when the Actor could not read the HTML (for example, a
  failed `htmlUrl` fetch)

The summary record holds `verdictCounts` for the run and `topBlockerRules`,
the blocker rules that fired most often across the batch.

### Safety bounds

- HTTP(S) only. No browser, no proxy, no third-party paid API.
- Every network target — an `htmlUrl` fetch and every asset HEAD request —
  is checked against a public-address allow list before each request and
  again on every redirect hop. Localhost, private, link-local, and reserved
  addresses are rejected.
- HTML input is capped at 2 MB for each email, 200 emails for each run.
- Fetches are bounded by timeout, byte size, and redirect count.
- Asset and email checks run under a bounded concurrency limit.

### Local development

```bash
uv sync
uv run pytest
uv run ruff check .
```

Run the Actor locally with the Apify CLI, or directly:

```bash
uv run python -m email_html_compatibility_checker
```

The default input at [`.actor/default_input.json`](.actor/default_input.json)
is a small, self-contained fixture: one email with a flexbox button, one
missing alt attribute, no preheader, and two stable public asset URLs (one
reachable, one a deliberate 404) so a first run shows both an `OK` and a
`BROKEN` asset finding.

# Actor input Schema

## `emails` (type: `array`):

One or more emails to check. Each item needs a name and one of html (the raw HTML) or htmlUrl (a public URL of the HTML). Maximum 200 items. Maximum 2 MB of HTML for each item. If you leave this field empty, the Actor checks the example email shown below.

## `clientProfiles` (type: `array`):

Email clients to score the HTML against. Defaults to all 12 supported profiles.

## `checkRemoteAssets` (type: `boolean`):

Send one HEAD request for each unique image and link URL that the HTML names. Turn this off to skip all network access.

## `maxAssetsPerEmail` (type: `integer`):

Cap on the number of unique remote assets that the Actor checks for each email. Extra assets past this cap are not checked.

## `darkModeChecks` (type: `boolean`):

Flag emails that have no color-scheme meta tag and no prefers-color-scheme rule. Clients with a dark mode may auto-invert the colors of such emails.

## `previousDatasetId` (type: `string`):

Optional. Dataset ID from an earlier run of this Actor. When set, each finding is marked as new or already known.

## Actor input object example

```json
{
  "emails": [
    {
      "name": "spring-sale-email",
      "html": "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>@media (max-width: 600px) { .stack { display: block !important; width: 100% !important; } } .cta { display: flex; }</style></head><body><table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\"><tr><td><img src=\"https://www.w3.org/Icons/w3c_home.png/" alt=\"Company logo\" width=\"120\"></td></tr><tr><td><img src=\"https://example.com/does-not-exist.png/" width=\"600\"></td></tr><tr><td class=\"cta\"><a href=\"https://example.com//">Shop now</a></td></tr></table></body></html>"
    }
  ],
  "clientProfiles": [
    "outlook-desktop-word",
    "outlook-com",
    "gmail-web",
    "gmail-android",
    "gmail-ios",
    "apple-mail-macos",
    "apple-mail-ios",
    "yahoo-web",
    "aol-web",
    "samsung-mail",
    "thunderbird",
    "orange-fr"
  ],
  "checkRemoteAssets": true,
  "maxAssetsPerEmail": 60,
  "darkModeChecks": true
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "emails": [
        {
            "name": "spring-sale-email",
            "html": "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>@media (max-width: 600px) { .stack { display: block !important; width: 100% !important; } } .cta { display: flex; }</style></head><body><table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\"><tr><td><img src=\"https://www.w3.org/Icons/w3c_home.png/" alt=\"Company logo\" width=\"120\"></td></tr><tr><td><img src=\"https://example.com/does-not-exist.png/" width=\"600\"></td></tr><tr><td class=\"cta\"><a href=\"https://example.com//">Shop now</a></td></tr></table></body></html>"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/email-html-compatibility-checker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "emails": [{
            "name": "spring-sale-email",
            "html": "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><style>@media (max-width: 600px) { .stack { display: block !important; width: 100% !important; } } .cta { display: flex; }</style></head><body><table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\"><tr><td><img src=\"https://www.w3.org/Icons/w3c_home.png/" alt=\"Company logo\" width=\"120\"></td></tr><tr><td><img src=\"https://example.com/does-not-exist.png/" width=\"600\"></td></tr><tr><td class=\"cta\"><a href=\"https://example.com//">Shop now</a></td></tr></table></body></html>",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("kingii98/email-html-compatibility-checker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "emails": [
    {
      "name": "spring-sale-email",
      "html": "<!DOCTYPE html><html><head><meta charset=\\"utf-8\\"><style>@media (max-width: 600px) { .stack { display: block !important; width: 100% !important; } } .cta { display: flex; }</style></head><body><table role=\\"presentation\\" width=\\"600\\" cellpadding=\\"0\\" cellspacing=\\"0\\"><tr><td><img src=\\"https://www.w3.org/Icons/w3c_home.png//" alt=\\"Company logo\\" width=\\"120\\"></td></tr><tr><td><img src=\\"https://example.com/does-not-exist.png//" width=\\"600\\"></td></tr><tr><td class=\\"cta\\"><a href=\\"https://example.com///">Shop now</a></td></tr></table></body></html>"
    }
  ]
}' |
apify call kingii98/email-html-compatibility-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/email-html-compatibility-checker"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/TFgLM1CM7fXWXVo9Q/builds/pJ8NI9PvUUUi1trWM/openapi.json
