Integrations
CI/CD Integration
Integrate Emailens into your continuous integration pipeline beyond GitHub Actions.
CLI (recommended)
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 lintIt 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"
doneGitLab 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/**/*.htmlUsing 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.