PHPackages                             rlnks/php-mail-audit - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. rlnks/php-mail-audit

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

rlnks/php-mail-audit
====================

Email HTML quality analysis engine — detect bad practices, score compatibility, get actionable insights before sending.

v1.3.8(2w ago)130↓100%MITPHPPHP ^8.1CI failing

Since May 10Pushed 1w agoCompare

[ Source](https://github.com/rlnks/php-mail-audit)[ Packagist](https://packagist.org/packages/rlnks/php-mail-audit)[ Docs](https://github.com/rlnks/php-mail-audit)[ RSS](/packages/rlnks-php-mail-audit/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (2)Versions (18)Used By (0)

php-mail-audit
==============

[](#php-mail-audit)

**Email HTML quality analysis engine for PHP.**

Analyze email templates before sending — detect compatibility issues, score your HTML against major email clients, and get actionable insights to fix problems before they reach your users' inboxes.

> "Grammarly for HTML emails"

[![License: MIT](https://camo.githubusercontent.com/784362b26e4b3546254f1893e778ba64616e362bd6ac791991d2c9e880a3a64e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e2e737667)](LICENSE)[![PHP](https://camo.githubusercontent.com/7535257ca228724c93658bd52583d4e47a9bab02c356abf6e54c1d575f2151e6/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e312532422d626c75652e737667)](https://www.php.net)

---

Table of Contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [Analyzing HTML](#analyzing-html)
- [Result Format](#result-format)
- [Bundled Rules](#bundled-rules)
- [Detection Types](#detection-types)
- [Localization](#localization)
- [Remote KB Sync](#remote-kb-sync)
- [CLI](#cli)
- [Custom Rules](#custom-rules)
- [Custom Detectors](#custom-detectors)
- [Score Calculation](#score-calculation)
- [Integration Examples](#integration-examples)
- [Running Tests](#running-tests)
- [License](#license)

---

Requirements
------------

[](#requirements)

- PHP 8.1 or higher
- No external dependencies — uses PHP's native `DOMDocument`
- No framework required — works with Laravel, Symfony, Slim, or plain PHP

---

Installation
------------

[](#installation)

```
composer require rlnks/php-mail-audit
```

---

Quick Start
-----------

[](#quick-start)

```
use MailAudit\MailAudit;

$html = file_get_contents('path/to/template.html');

$audit  = new MailAudit();
$result = $audit->analyze($html);

echo "Score: {$result['score']}/100\n";
echo "Issues: {$result['summary']['total_issues']}  |  Passed: {$result['summary']['passed']}\n\n";

foreach ($result['insights'] as $insight) {
    echo "[{$insight['severity']}] {$insight['message']}\n";
    echo "  Fix: {$insight['fix']}\n\n";
}

foreach ($result['passed'] as $check) {
    echo "[pass] {$check['message']}\n";
}
```

**Example output:**

```
Score: 84/100
Issues: 4  |  Passed: 12

[error] Form elements (, , ) are stripped or non-functional in virtually all email clients.
  Fix: Replace interactive forms with a CTA button linking to a landing page that hosts the form.

[warning] @import inside a  block is not supported in Gmail or Outlook.
  Fix: Replace @import with a  tag, and always define inline font-family stacks with web-safe fallbacks.

[info] External font detected — supported in Apple Mail and some modern clients, but not in Gmail or Outlook.
  Fix: Always define a font-family stack with web-safe fallbacks inline on every element.

[info] Div elements found — acceptable for wrapping content, but prefer  for layout in email.

[pass] No flexbox layout detected — good compatibility with Outlook desktop.
[pass] All images have explicit width and height attributes — layout will hold when images are blocked.
[pass] No external fonts detected — consistent rendering across all clients.
[pass] No JavaScript detected — email is safe for all clients and spam filters.

```

---

Configuration
-------------

[](#configuration)

All configuration is optional. The package works out of the box with the bundled rule set.

```
$audit = new MailAudit(
    config: [
        'auto_update' => true,                        // enable remote KB sync
        'ttl_days'    => 7,                           // cache TTL in days
        'endpoint'    => 'https://kb.example.com/rules.json',
        'api_key'     => getenv('MAILAUDIT_API_KEY'), // null = free tier
        'cache_path'  => '/tmp/mailaudit-rules.json', // writable path
    ],
    locale: 'en',          // single locale — 'en', 'fr', 'es', 'de', 'pt'
    // locale: ['en', 'fr'], // or multiple locales at once
);
```

### Config reference

[](#config-reference)

KeyTypeDefaultDescription`auto_update``bool``false`Enable remote KB fetch`ttl_days``int``7`Days before cache is considered stale`endpoint``string|null``null`Remote URL to fetch rules from`api_key``string|null``null`Bearer token sent in `Authorization` header`cache_path``string|null``null`Absolute path to the local cache file### Config file pattern

[](#config-file-pattern)

```
// config/mailaudit.php
return [
    'auto_update' => true,
    'ttl_days'    => 7,
    'endpoint'    => getenv('MAILAUDIT_ENDPOINT'),
    'api_key'     => getenv('MAILAUDIT_API_KEY'),
    'cache_path'  => __DIR__ . '/../var/mailaudit-rules.json',
];

// usage
$audit = new MailAudit(require __DIR__ . '/config/mailaudit.php');
```

---

Analyzing HTML
--------------

[](#analyzing-html)

```
$result = $audit->analyze(string $html): array
```

Pass the **raw HTML string** of the email template. The HTML does not need to be a complete document — partials and fragments are accepted.

```
// From a string
$result = $audit->analyze('Hello');

// From a file
$result = $audit->analyze(file_get_contents('emails/welcome.html'));

// From a rendered template (e.g. Twig)
$html   = $twig->render('emails/welcome.html.twig', $data);
$result = $audit->analyze($html);
```

---

Result Format
-------------

[](#result-format)

`analyze()` returns an array with four keys:

```
[
    'score'    => 81,          // int, 0–100
    'insights' => [ ... ],     // triggered rules (issues)
    'passed'   => [ ... ],     // rules that passed with a positive check message
    'summary'  => [ ... ],     // aggregate counts
]
```

### `score`

[](#score)

An integer between `0` and `100`. Higher is better. See [Score Calculation](#score-calculation).

### `insights`

[](#insights)

Each triggered rule produces one insight:

```
[
    'id'               => 'no-flexbox',
    'severity'         => 'error',          // 'error' | 'warning' | 'info'
    'weight'           => 15,               // nominal weight of the rule
    'message'          => 'Flexbox is not supported in Outlook desktop...',
    'fix'              => 'Replace flexbox with HTML table-based layout...',
    'affected_clients' => [
        'outlook_desktop' => ['supported' => false, 'versions' => 'all'],
        'gmail_web'       => ['supported' => false, 'versions' => '< 2022'],
        'apple_mail'      => ['supported' => true],
    ],
    'tags'      => ['css', 'layout'],
    'locations' => [
        ['line' => 12, 'column' => 5,  'offset_start' => 450,  'offset_end' => 471],
        ['line' => 34, 'column' => 9,  'offset_start' => 1205, 'offset_end' => 1226],
    ],
]
```

Each location entry points to one occurrence of the issue in the source HTML:

FieldTypeDescription`line``int`Line number (1-based)`column``int`Column within that line (1-based)`offset_start``int`Byte offset of the match start in the HTML string`offset_end``int`Byte offset of the match end (exclusive)This is designed for editor integration — use `offset_start`/`offset_end` with CodeMirror or Monaco `Range` objects to highlight the exact positions, and `line`/`column` to scroll and place the cursor.

```
// Reconstruct the matched snippet from offset
$snippet = substr($html, $loc['offset_start'], $loc['offset_end'] - $loc['offset_start']);
```

When multiple locales are requested, `message` and `fix` are associative arrays keyed by locale instead of strings:

```
// new MailAudit([], ['en', 'fr'])
'message' => [
    'en' => 'Flexbox is not supported in Outlook desktop...',
    'fr' => 'Flexbox n\'est pas supporté dans Outlook desktop...',
]
```

### `passed`

[](#passed)

Rules that did **not** trigger and carry a `success_message` appear here — useful for showing positive feedback alongside issues (similar to htmlemailcheck.com):

```
[
    'id'       => 'no-flexbox',
    'severity' => 'error',      // severity the rule would have had if triggered
    'message'  => 'No flexbox layout detected — good compatibility with Outlook desktop.',
    'tags'     => ['css', 'layout'],
]
```

Not every rule generates a passed item — only rules that define a `success_message` in their JSON (those where the absence of an issue is meaningfully good news).

### `summary`

[](#summary)

```
[
    'total_rules_checked' => 56,  // total rules evaluated
    'total_issues'        => 3,   // rules that fired
    'errors'              => 1,   // severity = error
    'warnings'            => 1,   // severity = warning
    'infos'               => 1,   // severity = info
    'passed'              => 9,   // rules that passed with a success message
]
```

---

Bundled Rules
-------------

[](#bundled-rules)

56 rules ship with the package. The philosophy: **flag bad usage, not feature presence**. Media queries, hover states, and class selectors used correctly (with inline fallbacks) score well. The engine penalizes the *absence* of fallbacks, not the features themselves.

### Errors — break rendering in major clients

[](#errors--break-rendering-in-major-clients)

Rule IDDescriptionWeight`no-flexbox`CSS `display: flex` not supported in Outlook15`no-grid`CSS `display: grid` not supported anywhere15`no-form-elements```, ``, `` stripped by all clients15`no-script``` stripped by all clients for security reasons15`no-iframe``` blocked by all clients15`no-svg`SVG not rendered in Outlook or Gmail12`no-video``` not supported in Outlook or Gmail12`no-audio``` not supported in any major client10`no-css-gap`CSS `gap` / `row-gap` / `column-gap` not supported anywhere9`no-object-fit``object-fit` not supported in any major client8`no-css-filter`CSS `filter` not supported in Outlook or Gmail8`no-clip-path``clip-path` not supported in any major client8`no-css-variables`CSS `var()` used **without a fallback value** — Outlook and Gmail silently ignore the property entirely7### Warnings — real problems when fallbacks are missing

[](#warnings--real-problems-when-fallbacks-are-missing)

Rule IDDescriptionWeight`style-no-inline-fallback``` block present but **zero** inline styles — layout breaks entirely when Gmail/Outlook strip the style block12`html-too-large`HTML exceeds 102 KB — Gmail clips the message and shows a "Message clipped" link10`media-no-inline-base``@media` queries present but no inline style baseline — responsive layout has no fallback for Gmail/Outlook10`img-dimensions``` without `width`/`height` — layout breaks when images are blocked8`no-float``float` breaks column layouts in Outlook 2007–20198`font-no-fallback`External font loaded but no inline `font-family` fallback stack — text falls back to client default when font is stripped8`no-picture``` / `srcset` not supported in Outlook or Gmail8`missing-alt-img``` without `alt` shows broken icons when images blocked7`no-css-calc``calc()` not supported in Outlook 2007–2019 or Gmail7`missing-https`HTTP (non-HTTPS) `src`/`href` detected — email clients block mixed content, breaking images and links6`no-div-layout``` with layout CSS (`width`, `float`, `margin`, etc.) — box model ignored by Outlook6`no-animation`CSS `animation` / `@keyframes` ignored by Outlook and Gmail6`url-unencoded`Unencoded space in a URL (`href` or `src`) — breaks the link in all clients5`css-at-import``@import` in `` silently ignored by Gmail/Outlook5`no-transform`CSS `transform` not supported in Outlook or Gmail5`css-at-import-no-link``@import` in `` with no `` fallback — font will not load in clients that strip `` blocks5`link-no-text``` with no accessible text or descriptive image `alt` — screen readers announce it as an unlabeled link5`text-image-ratio`Email is mostly images with very little readable text — high spam filter risk, renders blank when images are blocked6`email-max-width`Fixed-width `` over 600 px — overflows the Outlook rendering pane and narrow viewports5### Info — usage noted, minimal score impact

[](#info--usage-noted-minimal-score-impact)

Rules in this category flag the **presence** of a feature that is often used correctly as progressive enhancement. They fire when the feature is detected, regardless of fallback quality — the corresponding warning-level rules handle the bad cases.

Rule IDDescriptionWeight`no-position-absolute``position: absolute/fixed` ignored in most clients5`no-border-radius``border-radius` ignored by Outlook desktop4`no-box-shadow``box-shadow` not supported in Outlook3`no-transition`CSS `transition` not supported in Outlook or Gmail3`table-role-presentation`Layout tables without `role="presentation"` confuse screen readers3`preheader-missing`No preheader div found — inbox preview will show unrelated body text3`inline-css``` block present — acceptable when inline fallback styles are defined2`css-class-selectors`Class-based CSS in `` — Gmail strips `class` attributes2`css-media-queries``@media` queries detected — great when paired with inline styles2`no-external-fonts`External font loaded — supported in Apple Mail, not Gmail/Outlook2`missing-lang``` without `lang` attribute — screen readers and translation tools rely on it2`missing-viewport`No `` — mobile clients may render at desktop width2`preheader-too-long`Preheader text exceeds 150 characters (filler excluded) — most clients truncate at 85–150 chars2`css-pseudo-selectors``:hover`, `:focus` etc. detected — ignored in Outlook/Gmail, use as enhancement only1`div-content``` used as content wrapper — acceptable, but `` preferred for compatibility1`empty-alt-img``` detected — verify image is truly decorative and carries no information1`nbsp-missing`Regular space between a number and a currency/unit symbol — may break across lines on narrow screens1`heading-order`Heading levels skipped (e.g. `` directly followed by ``) — hurts accessibility and screen reader navigation2`tracking-pixel`1×1 tracking pixel detected — note that Apple Mail Privacy Protection may trigger false open events0`font-family-unquoted`Multi-word font name used without quotes in `font-family` — may be misinterpreted by some CSS parsers2`missing-charset`No character encoding declaration in `` — some clients may misrender special characters2`missing-doctype`No `` declaration — some clients fall back to quirks mode rendering2`table-cellspacing``` without `cellpadding="0" cellspacing="0"` — default cell spacing varies across clients2`missing-body-bgcolor`No background color on `` — some clients display a grey or off-white default background1---

Detection Types
---------------

[](#detection-types)

Every rule declares a `detection` object that specifies how the engine finds the issue. All detectors return exact character positions (line, column, byte offsets) for every match — see [`locations`](#insights) in the result format.

### `css_property`

[](#css_property)

Matches CSS patterns anywhere in the document — inline `style=""` attributes and `` blocks.

```
{
  "type": "css_property",
  "patterns": ["display: flex", "display:flex"]
}
```

Supports optional `"regex": true` for patterns that require precision (e.g. to avoid false positives with similar property names):

```
{
  "type": "css_property",
  "regex": true,
  "patterns": ["(?\\s*:"]
}
```

### `html_tag`

[](#html_tag)

Fires when the specified HTML tag is present at least once. Uses `DOMDocument` for accurate parsing.

```
{
  "type": "html_tag",
  "patterns": ["div", "svg", "form"]
}
```

Patterns are **tag names** (no angle brackets).

### `html_attribute_missing`

[](#html_attribute_missing)

Fires when at least one instance of `tag` is missing a required attribute, or has an attribute with the wrong value.

```
{
  "type": "html_attribute_missing",
  "tag": "img",
  "attributes": ["width", "height"]
}
```

With value check:

```
{
  "type": "html_attribute_missing",
  "tag": "table",
  "attributes": ["role"],
  "attribute_value": "presentation"
}
```

With `"only_empty": true` — fires when the attribute is present but empty (`alt=""`). Useful for distinguishing missing alt text from intentionally empty alt text:

```
{
  "type": "html_attribute_missing",
  "tag": "img",
  "attributes": ["alt"],
  "only_empty": true
}
```

### `html_content`

[](#html_content)

Matches arbitrary string patterns anywhere in the raw HTML string.

```
{
  "type": "html_content",
  "patterns": ["fonts.googleapis.com", "@import url"]
}
```

Supports `"regex": true` for patterns requiring precision. The `~` character is used as the regex delimiter internally — escape it as `\\~` if needed in a pattern:

```
{
  "type": "html_content",
  "regex": true,
  "patterns": ["src\\s*=\\s*[\"']http://", "href\\s*=\\s*[\"']http://"]
}
```

### `html_tag_with_style`

[](#html_tag_with_style)

Fires when a tag is present **and** its inline `style` attribute contains one of the given CSS patterns. Useful for distinguishing structural divs from decorative ones.

```
{
  "type": "html_tag_with_style",
  "tag": "div",
  "css_patterns": ["width:", "float:", "margin:"]
}
```

Supports `"regex": true` for precise matching (e.g. to avoid matching `max-width:` when looking for `width:`):

```
{
  "type": "html_tag_with_style",
  "tag": "div",
  "regex": true,
  "css_patterns": ["(?\\s*:\\s*(?!0)", "float\\s*:"]
}
```

### `correlation`

[](#correlation)

Fires when a **trigger** pattern is present but an expected **fallback** pattern is absent. Use this to flag bad *usage* of a feature rather than its mere presence.

```
{
  "type": "correlation",
  "trigger": {
    "type": "html_content",
    "patterns": ["fonts.googleapis.com", "@font-face"]
  },
  "fallback": {
    "type": "css_property",
    "regex": true,
    "patterns": ["font-family\\s*:[^;\"']*,"]
  }
}
```

The rule above fires only when an external font is loaded **and** no inline `font-family` fallback stack is found — correctly scoring emails that use custom fonts with proper fallbacks.

### `style_block`

[](#style_block)

Searches exclusively inside `` block content. Supports plain strings or regular expressions.

```
{
  "type": "style_block",
  "regex": false,
  "patterns": ["@media", "@import", ":hover"]
}
```

With regex:

```
{
  "type": "style_block",
  "regex": true,
  "patterns": ["\\.([a-zA-Z_-][\\w-]*)\\s*[{,:\\[]"]
}
```

### `preheader`

[](#preheader)

Detects the standard email preheader pattern — a `` with both `display:none` and `overflow:hidden` in its inline `style`. Two modes:

- **`missing`** — fires when no preheader div is found in a complete document (one that contains a `` tag). Fragments are skipped.
- **`too_long`** — fires when the visible preheader text exceeds `max_length` characters. Filler characters (`&nbsp;`, `&zwnj;`, and their Unicode equivalents) are stripped before measuring, so filling with spacers does not inflate the count.

```
{
  "type": "preheader",
  "mode": "missing"
}
```

```
{
  "type": "preheader",
  "mode": "too_long",
  "max_length": 150
}
```

### `html_metric`

[](#html_metric)

Measures a numeric property of the HTML and fires when it exceeds a threshold. Currently supported metrics:

MetricDescription`size`Total byte length of the HTML string (`strlen`)```
{
  "type": "html_metric",
  "metric": "size",
  "threshold": 102400
}
```

### `heading_order`

[](#heading_order)

Detects heading levels that are skipped in document order (e.g. `` directly followed by ``). No configuration options.

```
{
  "type": "heading_order"
}
```

### `html_link_no_text`

[](#html_link_no_text)

Fires when an `` element has no accessible text — no text node content and no child `` with a non-empty `alt`. No configuration options.

```
{
  "type": "html_link_no_text"
}
```

### `html_tracking_pixel`

[](#html_tracking_pixel)

Detects `` elements with `width="1"` and `height="1"`, the classic open-tracking pattern. No configuration options.

```
{
  "type": "html_tracking_pixel"
}
```

### `css_font_family`

[](#css_font_family)

Detects multi-word font names used without quotes in `font-family` declarations (e.g. `font-family: Open Sans` instead of `font-family: 'Open Sans'`). No configuration options.

```
{
  "type": "css_font_family"
}
```

### `html_table_width`

[](#html_table_width)

Fires when a `` element carries an inline `width` in pixels that exceeds `max_width` (default: 600).

```
{
  "type": "html_table_width",
  "max_width": 600
}
```

---

Localization
------------

[](#localization)

Five locales are bundled: `en` (English), `fr` (French), `es` (Spanish), `de` (German), `pt` (Portuguese).

### Single locale

[](#single-locale)

```
$audit = new MailAudit(locale: 'fr'); // or 'es', 'de', 'pt'

$result = $audit->analyze($html);
// $result['insights'][0]['message'] → string in French
// $result['insights'][0]['fix']     → string in French
```

If a locale is missing for a rule, it falls back to `en` automatically.

### Multiple locales

[](#multiple-locales)

Pass an array to receive all translations in a single pass:

```
$audit = new MailAudit(locale: ['en', 'fr']);

$result = $audit->analyze($html);
// $result['insights'][0]['message'] → ['en' => '...', 'fr' => '...']
// $result['insights'][0]['fix']     → ['en' => '...', 'fr' => '...']
// $result['passed'][0]['message']   → ['en' => '...', 'fr' => '...']
```

This is useful when building multi-language UIs without running `analyze()` twice.

### Adding a locale

[](#adding-a-locale)

Add the locale key to `message`, `fix`, and optionally `success_message` in each rule JSON:

```
{
  "message": {
    "en": "Flexbox is not supported in Outlook.",
    "fr": "Flexbox n'est pas supporté dans Outlook.",
    "de": "Flexbox wird in Outlook nicht unterstützt."
  },
  "fix": {
    "en": "Use HTML tables for layout.",
    "fr": "Utilisez des tables HTML pour la mise en page.",
    "de": "Verwenden Sie HTML-Tabellen für das Layout."
  }
}
```

---

Remote KB Sync
--------------

[](#remote-kb-sync)

By default the package uses the bundled rule set. You can point it at a remote endpoint to receive updated rules without a Composer update.

### How it works

[](#how-it-works)

```
Remote endpoint
      ↓  fetched when cache is stale or missing
Local cache file  (cache_path)
      ↓  fallback if fetch fails or auto_update = false
Bundled rules  (rules/*.json in the package)

```

### Enabling sync

[](#enabling-sync)

```
$audit = new MailAudit([
    'auto_update' => true,
    'ttl_days'    => 7,                                        // re-fetch after 7 days
    'endpoint'    => 'https://kb.mailaudit.io/rules.json',
    'api_key'     => getenv('MAILAUDIT_API_KEY'),              // optional, pro tier
    'cache_path'  => __DIR__ . '/var/mailaudit-rules.json',    // must be writable
]);
```

### Tier behavior

[](#tier-behavior)

ConditionRules returnedNo `api_key`Free rules onlyValid `api_key`Free + Pro rulesExpired / invalid `api_key`401 response → silent fallback to bundled rules### Cache behavior

[](#cache-behavior)

SituationBehaviorFirst install, no cacheBundled rulesCache exists, not staleCache usedCache stale or missingFetch from endpoint, write cacheFetch fails (network error)Bundled rules (silent fallback)`auto_update = false`Always bundled rules---

CLI
---

[](#cli)

A command-line tool is available at `vendor/bin/mailaudit` after installation.

### `sync` — refresh the local cache

[](#sync--refresh-the-local-cache)

```
vendor/bin/mailaudit sync [options]
```

**Using environment variables:**

```
export MAILAUDIT_ENDPOINT=https://kb.mailaudit.io/rules.json
export MAILAUDIT_API_KEY=your-api-key
export MAILAUDIT_CACHE_PATH=/var/cache/mailaudit-rules.json

vendor/bin/mailaudit sync
```

**Using a config file:**

```
vendor/bin/mailaudit sync --config=config/mailaudit.php
```

**Dry run** (fetch but do not write cache):

```
vendor/bin/mailaudit sync --config=config/mailaudit.php --dry-run
```

### Available options

[](#available-options)

OptionDescription`--config=`PHP file returning a config array`--dry-run`Fetch without writing the cache### Environment variables

[](#environment-variables)

VariableDescription`MAILAUDIT_ENDPOINT`Remote KB endpoint URL`MAILAUDIT_API_KEY`API key for pro tier`MAILAUDIT_CACHE_PATH`Absolute path to the local cache file---

### `audit` — analyze an email file

[](#audit--analyze-an-email-file)

```
vendor/bin/mailaudit audit path/to/email.html
vendor/bin/mailaudit audit path/to/email.html --locale=fr
vendor/bin/mailaudit audit path/to/email.html --format=json
```

**Example output (text):**

```
SCORE: 84/100 — email.html
────────────────────────────────────────────
[ERROR  ] no-flexbox                 Flexbox is not supported in Outlook...
[WARN   ] missing-https              HTTP links detected — email clients b...
[INFO   ] div-content                 used as content wrapper — acce...
────────────────────────────────────────────
✓ no-script   ✓ img-dimensions   ✓ table-role-presentation

```

**With `--format=json`**, the full `analyze()` result array is printed as pretty-printed JSON — same structure as documented in [Result Format](#result-format).

### `audit` options

[](#audit-options)

OptionDescription`--locale=`Locale for messages: `en` (default), `fr`, `es`, `de`, `pt``--format=json`Output the full result as JSON instead of the formatted summary---

Custom Rules
------------

[](#custom-rules)

You can add your own rules without modifying the package.

### 1. Create a rule JSON file

[](#1-create-a-rule-json-file)

```
{
  "id": "no-video",
  "version": "1.0",
  "updated_at": "2026-05-09",
  "source": "https://www.caniemail.com/features/html-video/",
  "tier": "free",
  "severity": "error",
  "weight": 12,
  "tags": ["html", "media"],
  "detection": {
    "type": "html_tag",
    "patterns": ["video"]
  },
  "affected_clients": {
    "outlook_desktop": { "supported": false, "versions": "all" },
    "gmail_web": { "supported": false }
  },
  "message": {
    "en": " elements are not supported in Outlook or Gmail.",
    "fr": "Les éléments  ne sont pas supportés dans Outlook ni Gmail."
  },
  "fix": {
    "en": "Use a linked image (GIF or static) as a fallback for video content.",
    "fr": "Utiliser une image liée (GIF ou statique) comme fallback pour le contenu vidéo."
  }
}
```

### 2. Load it alongside the bundled rules

[](#2-load-it-alongside-the-bundled-rules)

```
use MailAudit\Loader\RuleLoader;
use MailAudit\Analysis\RuleEngine;
use MailAudit\Analysis\ScoringEngine;
use MailAudit\Feedback\FeedbackGenerator;

$bundled = (new RuleLoader())->load();
$custom  = [json_decode(file_get_contents('rules/no-video.json'), true)];
$rules   = array_merge($bundled, $custom);

$triggered = (new RuleEngine($rules))->analyze($html);
$score     = (new ScoringEngine())->calculate($triggered);
$insights  = (new FeedbackGenerator('en'))->generate($triggered);
```

Or subclass `MailAudit` to make this reusable in your project.

### Rule JSON reference

[](#rule-json-reference)

FieldTypeRequiredDescription`id``string`YesUnique identifier`version``string`YesSemver string, bumped on changes`updated_at``string`YesISO date `YYYY-MM-DD``source``string`NoReference URL (e.g. caniemail.com)`tier``string`Yes`free` or `pro``severity``string`Yes`error`, `warning`, or `info``weight``int`YesPoints deducted from score (0–100)`tags``string[]`NoCategorization tags`detection``object`YesSee [Detection Types](#detection-types)`affected_clients``object`NoPer-client support data`message``object`YesLocale-keyed problem description`fix``object`YesLocale-keyed fix suggestion`success_message``object`NoLocale-keyed message shown when the rule passes. When present, the rule appears in the `passed` array of the result.---

Custom Detectors
----------------

[](#custom-detectors)

You can register new detection types to support custom rule patterns.

### 1. Implement `DetectorInterface`

[](#1-implement-detectorinterface)

```
use MailAudit\Detection\DetectorInterface;

class MjmlTagDetector implements DetectorInterface
{
    public function matches(string $html, array $detection): bool
    {
        foreach ($detection['tags'] ?? [] as $tag) {
            if (str_contains($html, "analyze(file_get_contents('templates/welcome.html'));
      if (\$result['score'] < 70) {
          echo 'Email quality score too low: ' . \$result['score'] . '/100\n';
          exit(1);
      }
      echo 'Score: ' . \$result['score'] . "/100 — OK\n";
    "
```

---

Running Tests
-------------

[](#running-tests)

```
composer install
vendor/bin/phpunit
```

Run static analysis:

```
vendor/bin/phpstan analyse
```

---

License
-------

[](#license)

[MIT](LICENSE) — © 2026 rlnks

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance97

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

14

Last Release

19d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/245694487?v=4)[philippegagnon-sys](/maintainers/philippegagnon-sys)[@philippegagnon-sys](https://github.com/philippegagnon-sys)

---

Top Contributors

[![philippegagnon-sys](https://avatars.githubusercontent.com/u/245694487?v=4)](https://github.com/philippegagnon-sys "philippegagnon-sys (19 commits)")

---

Tags

composeremailemail-compatibilitygmailhtmllinteroutlookphpcompatibilityemailhtmlqualityAuditgmailoutlook

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/rlnks-php-mail-audit/health.svg)

```
[![Health](https://phpackages.com/badges/rlnks-php-mail-audit/health.svg)](https://phpackages.com/packages/rlnks-php-mail-audit)
```

###  Alternatives

[soundasleep/html2text

A PHP script to convert HTML into a plain text format

48520.6M84](/packages/soundasleep-html2text)[snowfire/beautymail

Send beautiful html emails with Laravel

1.3k746.6k2](/packages/snowfire-beautymail)[voku/html2text

Only a Fork of -&gt; html2text: Converts HTML to formatted plain text

40340.6k2](/packages/voku-html2text)[osiemsiedem/laravel-autolink

A Laravel package for converting URLs in a given string of text into clickable links.

13135.2k](/packages/osiemsiedem-laravel-autolink)[swissup/module-email

Magento2 email providers integration (smtp, mandrill, amazon ses)

1413.3k1](/packages/swissup-module-email)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
