Emailens
Integrations

CI/CD Integration

Integrate Emailens into your continuous integration pipeline beyond GitHub Actions.

The CLI runs the engine on the CI machine. No API key, no quota, no network call:

find emails -name '*.html' -print0 | xargs -0 -n1 npx -y @emailens/cli lint

It exits non-zero when any file has an error. See the CLI reference for --fail-on-warning, --max-warnings, and --skip.

Generic CI (curl-based)

Any CI system can call the Emailens API. Send an API key: without one the endpoint allows 3 previews a day per IP, and CI runners share IPs.

#!/bin/bash
THRESHOLD=70
API_KEY="${EMAILENS_API_KEY}"

for file in emails/*.html; do
  HTML=$(cat "$file")
  RESPONSE=$(curl -s -X POST https://app.emailens.dev/api/preview \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $API_KEY" \
    -d "{\"html\": $(echo "$HTML" | jq -Rs .)}")

  OVERALL=$(echo "$RESPONSE" | jq '.overallScore')

  if [ "$OVERALL" -lt "$THRESHOLD" ]; then
    echo "FAIL: $file scored $OVERALL (threshold: $THRESHOLD)"
    exit 1
  fi

  echo "PASS: $file scored $OVERALL"
done

GitLab CI

email-check:
  stage: test
  image: curlimages/curl:latest
  script:
    - |
      for file in emails/*.html; do
        HTML=$(cat "$file")
        SCORE=$(curl -s -X POST https://app.emailens.dev/api/preview \
          -H "Content-Type: application/json" \
          -H "Authorization: Bearer $EMAILENS_API_KEY" \
          -d "{\"html\": $(echo "$HTML" | jq -Rs .)}" | jq '.overallScore')
        echo "$file: $SCORE/100"
        [ "$SCORE" -lt 70 ] && exit 1
      done
  only:
    changes:
      - emails/**/*.html

Using the engine directly

For faster CI (no API calls), install @emailens/engine and run analysis locally:

import { analyzeEmail, generateCompatibilityScore } from "@emailens/engine";
import { readFileSync, readdirSync } from "fs";

const files = readdirSync("emails").filter((f) => f.endsWith(".html"));
let failed = false;

for (const file of files) {
  const html = readFileSync(`emails/${file}`, "utf-8");
  const warnings = analyzeEmail(html);
  const scores = generateCompatibilityScore(warnings);

  // Gate on the worst client, as the API and CLI do. An average hides a
  // broken Outlook behind twenty clients that render fine.
  const worst = Math.min(...Object.values(scores).map((s) => s.score));

  if (worst < 70) {
    console.error(`FAIL: ${file} scored ${worst}`);
    failed = true;
  } else {
    console.log(`PASS: ${file} scored ${worst}`);
  }
}

if (failed) process.exit(1);

This approach is faster since it runs locally without network calls, but doesn't include screenshots.

On this page