定制 rlnks/php-mail-audit 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

rlnks/php-mail-audit

最新稳定版本:v1.2.1

Composer 安装命令:

composer require rlnks/php-mail-audit

包简介

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

README 文档

README

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 PHP

Table of Contents

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

composer require rlnks/php-mail-audit

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 (<form>, <input>, <button>) 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 <style> block is not supported in Gmail or Outlook.
  Fix: Replace @import with a <link> 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 <td> 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

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

Key Type Default Description
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/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

$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('<div style="display:flex;">Hello</div>');

// 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

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

An integer between 0 and 100. Higher is better. See Score Calculation.

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:

Field Type Description
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

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

[
    'total_rules_checked' => 32,  // 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

36 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

Rule ID Description Weight
no-flexbox CSS display: flex not supported in Outlook 15
no-grid CSS display: grid not supported anywhere 15
no-form-elements <form>, <input>, <button> stripped by all clients 15
no-script <script> stripped by all clients for security reasons 15
no-iframe <iframe> blocked by all clients 15
no-svg SVG not rendered in Outlook or Gmail 12
no-video <video> not supported in Outlook or Gmail 12
no-audio <audio> not supported in any major client 10
no-css-gap CSS gap / row-gap / column-gap not supported anywhere 9
no-object-fit object-fit not supported in any major client 8
no-css-filter CSS filter not supported in Outlook or Gmail 8
no-clip-path clip-path not supported in any major client 8

Warnings — real problems when fallbacks are missing

Rule ID Description Weight
style-no-inline-fallback <style> block present but zero inline styles — layout breaks entirely when Gmail/Outlook strip the style block 12
media-no-inline-base @media queries present but no inline style baseline — responsive layout has no fallback for Gmail/Outlook 10
img-dimensions <img> without width/height — layout breaks when images are blocked 8
no-float float breaks column layouts in Outlook 2007–2019 8
font-no-fallback External font loaded but no inline font-family fallback stack — text falls back to client default when font is stripped 8
no-picture <picture> / srcset not supported in Outlook or Gmail 8
missing-alt-img <img> without alt shows broken icons when images blocked 7
no-css-calc calc() not supported in Outlook 2007–2019 or Gmail 7
no-css-variables CSS var(--x) not supported in Outlook or Gmail 7
no-div-layout <div> with layout CSS (width, float, margin, etc.) — box model ignored by Outlook 6
no-animation CSS animation / @keyframes ignored by Outlook and Gmail 6
css-at-import @import in <style> silently ignored by Gmail/Outlook 5
no-transform CSS transform not supported in Outlook or Gmail 5

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 ID Description Weight
no-position-absolute position: absolute/fixed ignored in most clients 5
no-border-radius border-radius ignored by Outlook desktop 4
no-box-shadow box-shadow not supported in Outlook 3
no-transition CSS transition not supported in Outlook or Gmail 3
table-role-presentation Layout tables without role="presentation" confuse screen readers 3
inline-css <style> block present — acceptable when inline fallback styles are defined 2
css-class-selectors Class-based CSS in <style> — Gmail strips class attributes 2
css-media-queries @media queries detected — great when paired with inline styles 2
no-external-fonts External font loaded — supported in Apple Mail, not Gmail/Outlook 2
css-pseudo-selectors :hover, :focus etc. detected — ignored in Outlook/Gmail, use as enhancement only 1
div-content <div> used as content wrapper — acceptable, but <td> preferred for compatibility 1

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 in the result format.

css_property

Matches CSS patterns anywhere in the document — inline style="" attributes and <style> 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": ["(?<![a-z-])transform\\s*:"]
}

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

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"
}

html_content

Matches arbitrary string patterns anywhere in the raw HTML string.

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

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": ["(?<![a-z-])width\\s*:\\s*(?!0)", "float\\s*:"]
}

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

Searches exclusively inside <style> 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*[{,:\\[]"]
}

Localization

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

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

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

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

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

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

$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

Condition Rules returned
No api_key Free rules only
Valid api_key Free + Pro rules
Expired / invalid api_key 401 response → silent fallback to bundled rules

Cache behavior

Situation Behavior
First install, no cache Bundled rules
Cache exists, not stale Cache used
Cache stale or missing Fetch from endpoint, write cache
Fetch fails (network error) Bundled rules (silent fallback)
auto_update = false Always bundled rules

CLI

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

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

Option Description
--config=<path> PHP file returning a config array
--dry-run Fetch without writing the cache

Environment variables

Variable Description
MAILAUDIT_ENDPOINT Remote KB endpoint URL
MAILAUDIT_API_KEY API key for pro tier
MAILAUDIT_CACHE_PATH Absolute path to the local cache file

Custom Rules

You can add your own rules without modifying the package.

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": "<video> elements are not supported in Outlook or Gmail.",
    "fr": "Les éléments <video> 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

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

Field Type Required Description
id string Yes Unique identifier
version string Yes Semver string, bumped on changes
updated_at string Yes ISO date YYYY-MM-DD
source string No Reference URL (e.g. caniemail.com)
tier string Yes free or pro
severity string Yes error, warning, or info
weight int Yes Points deducted from score (0–100)
tags string[] No Categorization tags
detection object Yes See Detection Types
affected_clients object No Per-client support data
message object Yes Locale-keyed problem description
fix object Yes Locale-keyed fix suggestion
success_message object No Locale-keyed message shown when the rule passes. When present, the rule appears in the passed array of the result.

Custom Detectors

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

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, "<mj-{$tag}")) {
                return true;
            }
        }
        return false;
    }
}

2. Register it with the factory

use MailAudit\Detection\DetectorFactory;

DetectorFactory::register('mjml_tag', MjmlTagDetector::class);

3. Use it in a rule JSON

{
  "detection": {
    "type": "mjml_tag",
    "tags": ["section", "column"]
  }
}

Registration is global and static — register once at application bootstrap before calling analyze().

Score Calculation

The score starts at 100. Each triggered rule deducts a weighted amount based on its severity:

deduction = weight × severity_multiplier

severity multipliers:
  error   → 1.0  (full weight)
  warning → 0.6  (60% of weight)
  info    → 0.3  (30% of weight)

score = max(0, round(100 - sum(deductions)))

Example:

Rule triggered Severity Weight Multiplier Deduction
no-svg error 12 × 1.0 12.0
style-no-inline-fallback warning 12 × 0.6 7.2
no-css-calc warning 7 × 0.6 4.2
css-media-queries info 2 × 0.3 0.6
Total deduction 24.0
Final score 76 / 100

The score cannot go below 0.

Severity vs weight

Severity (error, warning, info) is a qualitative label indicating the nature of the problem. Weight is the nominal importance of the rule. The multiplier ensures that warnings and info items have a proportionally smaller impact on the score than blocking errors, so a well-crafted email with minor compatibility caveats scores realistically (75–90) rather than being penalized alongside fundamentally broken emails.

Integration Examples

Vanilla PHP

use MailAudit\MailAudit;

$result = (new MailAudit())->analyze(file_get_contents('email.html'));

if ($result['score'] < 80) {
    foreach ($result['insights'] as $insight) {
        if ($insight['severity'] === 'error') {
            throw new RuntimeException("Email has blocking issues: {$insight['message']}");
        }
    }
}

Symfony

// config/services.yaml
services:
    MailAudit\MailAudit:
        arguments:
            $config:
                auto_update: true
                ttl_days: 7
                endpoint: '%env(MAILAUDIT_ENDPOINT)%'
                api_key: '%env(MAILAUDIT_API_KEY)%'
                cache_path: '%kernel.cache_dir%/mailaudit-rules.json'

// In a service or controller
public function __construct(private MailAudit $audit) {}

public function preview(string $html): array
{
    return $this->audit->analyze($html);
}

Laravel

// config/mailaudit.php
return [
    'auto_update' => true,
    'ttl_days'    => 7,
    'endpoint'    => env('MAILAUDIT_ENDPOINT'),
    'api_key'     => env('MAILAUDIT_API_KEY'),
    'cache_path'  => storage_path('app/mailaudit-rules.json'),
];

// AppServiceProvider
$this->app->singleton(\MailAudit\MailAudit::class, fn() =>
    new \MailAudit\MailAudit(config('mailaudit'))
);

In a CI/CD pipeline (GitHub Actions)

- name: Audit email templates
  run: |
    php -r "
      require 'vendor/autoload.php';
      \$audit  = new \MailAudit\MailAudit();
      \$result = \$audit->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

composer install
vendor/bin/phpunit

Run static analysis:

vendor/bin/phpstan analyse

License

MIT — © 2026 rlnks

统计信息

  • 总下载量: 4
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-05-10

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固