Comprehensive audit of all transactional, drip, and marketing email templates — CAN-SPAM compliance, RFC 8058 one-click unsubscribe, HTML structure, responsive design, i18n coverage, and deliverability configuration. Use when asked to audit emails, email templates, email compliance, or deliverability.
Scanned 6/1/2026
Install to Claude Code
npx -y skills add lroy-stack/ai-pod-store --skill audit-email-templates --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Audit Email Templates?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/lroy-stack-audit-email-templates)More formats (shields.io, HTML) on the badges page.
---
name: Email Templates & Deliverability Audit
description: >
Comprehensive audit of all transactional, drip, and marketing email templates — CAN-SPAM compliance,
RFC 8058 one-click unsubscribe, HTML structure, responsive design, i18n coverage, and deliverability
configuration. Use when asked to audit emails, email templates, email compliance, or deliverability.
allowed-tools: Read, Grep, Glob, Bash
---
# Email Templates & Deliverability Audit
Systematic audit of all email-sending code paths: transactional order emails, drip sequences, abandoned cart recovery, newsletter, and webhook-triggered emails. Covers legal compliance, HTML quality, i18n, brand consistency, and deliverability.
## Prerequisites
Before starting, read these files for architectural context:
- `CLAUDE.md` — component standards, Supabase connection reference, environment variables
- `frontend/src/lib/resend.ts` — main email service (6 transactional templates, 821 lines)
- `frontend/src/lib/email-drip.ts` — drip queue system (78 lines)
## File Map
Keep this reference open throughout the audit. All paths relative to `frontend/src/`.
### Core Email Service
| File | Role |
|---|---|
| `lib/resend.ts` | 6 transactional email functions: sendOrderConfirmationEmail (50-174), sendOrderShippedEmail (179-294), sendOrderCancelledEmail (299-429), sendOrderDeliveredEmail (434-563), sendOrderFailedEmail (568-675), sendCreditPurchaseEmail (680-821) |
| `lib/resend.ts:12-24` | EMAIL_COLORS constant — shared color palette for all templates |
### Drip System
| File | Role |
|---|---|
| `lib/email-drip.ts` | Welcome drip sequence: 1h, 72h, 168h delays. Uses `drip_queue` table |
| `app/api/cron/drip/route.ts` | Drip cron handler (192 lines). Templates: welcome, tips, credit_offer. HAS RFC 8058 headers (lines 149-150) |
### Abandoned Cart Recovery
| File | Role |
|---|---|
| `app/api/cron/abandoned-cart-recovery/route.ts` | 2 recovery emails at 1h and 24h (300+ lines). NO unsubscribe links, NO RFC 8058 headers |
### Newsletter
| File | Role |
|---|---|
| `app/api/newsletter/subscribe/route.ts` | Double opt-in subscription (178 lines), confirmation token |
| `app/api/newsletter/unsubscribe/route.ts` | Unsubscribe handler (263 lines) |
### Webhook-Triggered Emails
| File | Role |
|---|---|
| `lib/pod/webhooks/handlers/` | Order lifecycle webhook handlers that trigger transactional emails |
### Database Tables
| Table | Purpose |
|---|---|
| `drip_queue` | Pending drip emails (user_id, template, scheduled_at, sent_at) |
| `newsletter_subscribers` | Newsletter subscriptions (email, confirmed, token) |
---
## Workflow
Execute each phase sequentially. Record every finding with severity, file path, and line number.
### Phase 1: CAN-SPAM & Legal Compliance (10 checks)
#### 1.1 Unsubscribe Links — Transactional Emails
- Read `lib/resend.ts` — all 6 email functions
- **Check**: Do transactional emails include an unsubscribe link? (CAN-SPAM requires for commercial content mixed with transactional)
- **Check**: Is the unsubscribe mechanism functional (hits a real endpoint)?
- **Known Gap**: Transactional emails have NO unsubscribe links — verify current status
- **Severity**: FAIL if commercial content (upsells, promotions) is included without unsubscribe
#### 1.2 Unsubscribe Links — Drip Emails
- Read `app/api/cron/drip/route.ts`
- **Check**: Drip emails include unsubscribe links (lines 149-150 have RFC 8058 headers — verify link in body too)
- **Check**: Unsubscribe link leads to a working endpoint
- **Check**: One-click unsubscribe actually removes user from drip queue
#### 1.3 Unsubscribe Links — Abandoned Cart
- Read `app/api/cron/abandoned-cart-recovery/route.ts`
- **Check**: Abandoned cart emails include unsubscribe/opt-out link
- **Known Gap**: NO unsubscribe links, NO RFC 8058 headers — verify current status
- **Severity**: CRITICAL if missing — abandoned cart emails are commercial, CAN-SPAM requires unsubscribe
#### 1.4 RFC 8058 One-Click Unsubscribe Headers
- Search for `List-Unsubscribe`, `List-Unsubscribe-Post` across all email-sending files
- **Check**: Drip emails have RFC 8058 headers (verified in cron/drip)
- **Check**: Abandoned cart emails have RFC 8058 headers
- **Check**: Newsletter emails have RFC 8058 headers
- **Benchmark**: Gmail/Yahoo require RFC 8058 for bulk senders (>5000 emails/day) since Feb 2024
#### 1.5 Physical Address
- Search for address, street, postal, physical in email template HTML across all senders
- **Check**: Every commercial email includes a physical mailing address
- **Check**: Address is in the email footer (not hidden)
- **Check**: Address is consistent across all email types
#### 1.6 Sender Identification
- Search for `from:`, `From`, `reply`, `replyTo` in all email-sending code
- **Check**: From address uses a real domain (not noreply@gmail.com)
- **Check**: From name is the brand name (SKAPARA or configured brand)
- **Check**: Reply-To is set to a monitored address
- **Check**: From address is consistent across all email types
#### 1.7 Subject Line Compliance
- Read subject lines in all email functions
- **Check**: Subject lines are not deceptive or misleading
- **Check**: Subject lines are localized per user locale
- **Check**: No ALL CAPS subjects (spam trigger)
#### 1.8 GDPR Consent (EU Store)
- Search for `consent`, `opt_in`, `gdpr`, `marketing_consent` in newsletter and drip code
- **Check**: Newsletter uses double opt-in (confirmation email before adding to list)
- **Check**: Drip sequence only sends to users who consented to marketing
- **Check**: Consent timestamp is recorded in database
- **Severity**: CRITICAL for EU store — GDPR requires explicit consent for marketing emails
#### 1.9 Opt-Out Processing Time
- Read unsubscribe handler logic
- **Check**: Unsubscribe is processed immediately (not "within 10 days")
- **Check**: No further emails are sent after unsubscribe (check drip queue cleanup)
- **Benchmark**: CAN-SPAM allows 10 business days, but best practice is immediate
#### 1.10 Email Type Classification
- Review all email-sending code paths
- **Check**: Are emails correctly classified as transactional vs. commercial?
- **Check**: Credit purchase confirmation — is this purely transactional or does it include promotions?
- **Check**: Drip "credit_offer" template — this is commercial, verify it has full CAN-SPAM compliance
### Phase 2: HTML Email Structure & Rendering (8 checks)
#### 2.1 DOCTYPE & Character Encoding
- Read HTML output in each email function in `lib/resend.ts`
- **Check**: HTML starts with `<!DOCTYPE html>` and `<html>` tag
- **Check**: `<meta charset="utf-8">` is present
- **Check**: `<meta name="viewport" content="width=device-width, initial-scale=1.0">` is present
- **Benchmark**: Without DOCTYPE, Outlook renders in quirks mode
#### 2.2 Inline CSS
- Examine CSS in email HTML templates
- **Check**: All styles are inline (not in `<style>` block or external CSS)
- **Check**: No CSS classes that depend on `<style>` tag (Gmail strips `<style>` in non-AMP)
- **Check**: If `<style>` is used, it is duplicated as inline styles for fallback
- **Benchmark**: Gmail, Yahoo, and Outlook all handle inline CSS reliably
#### 2.3 Responsive Design
- Check email HTML for responsive patterns
- **Check**: Email uses `max-width: 600px` container (email client standard)
- **Check**: Images use `max-width: 100%` and `height: auto`
- **Check**: Font sizes are readable on mobile (min 14px body, 22px headings)
- **Check**: Buttons have minimum 44px tap targets
- **Benchmark**: 60%+ of emails are read on mobile
#### 2.4 Image Handling
- Search for `<img`, `src=`, image URLs in email HTML
- **Check**: All images have `alt` text
- **Check**: Images use absolute URLs (not relative paths)
- **Check**: Logo/brand images are hosted on reliable CDN (not localhost)
- **Check**: Email is readable with images disabled (text fallbacks)
#### 2.5 Table-Based Layout
- Check if emails use table-based layout (required for Outlook compatibility)
- **Check**: Main structure uses `<table>` elements (not `<div>` flex/grid)
- **Check**: Tables use `role="presentation"` for accessibility
- **Check**: Cell padding is used instead of margin (margin is unreliable in email)
- **Benchmark**: Outlook renders only table-based layouts correctly
#### 2.6 Dark Mode Support
- Search for `color-scheme`, `prefers-color-scheme`, `mso-`, dark mode patterns in email HTML
- **Check**: `<meta name="color-scheme" content="light dark">` is present
- **Check**: Dark mode overrides are provided (or colors are dark-mode-safe)
- **Check**: Logo has transparent background or dark mode variant
- **Benchmark**: 30%+ of users use dark mode in email clients
#### 2.7 Preview Text
- Search for preview text, preheader, hidden text patterns in email HTML
- **Check**: Each email has a preview/preheader text (the snippet shown in inbox list)
- **Check**: Preview text is not just a repeat of the subject line
- **Check**: Preview text is hidden from the email body (zero-size div technique)
- **Benchmark**: Preview text significantly impacts open rates
#### 2.8 Email Size
- Estimate total HTML size of each email template
- **Check**: Email HTML is under 100KB (Gmail clips emails over 102KB)
- **Check**: No large base64 inline images (use hosted URLs instead)
- **Check**: No unnecessary whitespace or comments in production HTML
### Phase 3: Internationalization (6 checks)
#### 3.1 Locale Parameter
- Read each email function signature in `lib/resend.ts`
- **Check**: All 6 transactional emails accept a `locale` parameter
- **Check**: Drip emails pass user's locale to templates
- **Check**: Abandoned cart emails use the user's preferred locale
- **Check**: Default locale fallback exists (en)
#### 3.2 Translation Coverage
- Read translation dictionaries in `lib/resend.ts` for each email
- **Check**: All 3 locales (en, es, de) are present for every email
- **Check**: Subject lines are translated
- **Check**: Body text is translated (not just subject)
- **Check**: Button/CTA text is translated
- **Check**: Footer text is translated
#### 3.3 Translation Quality
- Spot-check German and Spanish translations
- **Check**: No machine translation artifacts (awkward phrasing, wrong formality level)
- **Check**: Currency formatting matches locale (EUR with comma decimal for DE/ES)
- **Check**: Date formatting matches locale
- **Check**: Brand name "SKAPARA" is not translated (proper noun)
#### 3.4 Drip Template Translations
- Read drip cron route for template rendering
- **Check**: welcome, tips, credit_offer templates have en/es/de versions
- **Check**: Locale is stored in drip_queue or derived from user profile
#### 3.5 Abandoned Cart Translations
- Read abandoned cart recovery route
- **Check**: Recovery emails support all 3 locales
- **Check**: Product names in cart are displayed in user's locale (if translated)
#### 3.6 RTL Support
- **Check**: Is RTL (right-to-left) considered? (Not required for en/es/de but good for future ar/he)
- **Note**: Low priority for current locales, but flag if HTML structure would break with `dir="rtl"`
### Phase 4: Brand Consistency (5 checks)
#### 4.1 Color Palette
- Read `EMAIL_COLORS` constant in `lib/resend.ts:12-24`
- **Check**: Colors are defined in a single constant (not hardcoded per email)
- **Check**: Colors match brand guidelines (cross-reference with BRAND_IDENTITY.md)
- **Check**: All emails use EMAIL_COLORS (no rogue hex values)
- **Check**: Sufficient contrast ratios for accessibility (WCAG AA: 4.5:1 for text)
#### 4.2 Logo & Branding
- Search for logo, brand, header in email HTML
- **Check**: Brand logo appears in every email header
- **Check**: Logo links to the store homepage
- **Check**: Logo URL is absolute and hosted on CDN
- **Check**: Brand name appears consistently (SKAPARA, not variations)
#### 4.3 Footer Consistency
- Compare footer content across all email types
- **Check**: Footer includes: brand name, physical address, unsubscribe (where required), social links
- **Check**: Footer is identical across email types (single source of truth)
- **Check**: Social media links are present and correct
- **Check**: Copyright year is dynamic (not hardcoded)
#### 4.4 Tone & Voice
- Read copy across all email types
- **Check**: Tone is consistent (friendly, professional, on-brand)
- **Check**: Order status emails are clear and action-oriented
- **Check**: Error/failure emails are empathetic (not robotic)
- **Check**: CTAs use action verbs ("Track Your Order", "Shop Now")
#### 4.5 Template Reuse
- Check if emails share a common layout wrapper
- **Check**: Is there a shared email layout function/template (header + footer wrapper)?
- **Check**: Or does each email duplicate the full HTML structure?
- **Recommendation**: DRY email layout reduces inconsistency risk
### Phase 5: Error Handling & Reliability (6 checks)
#### 5.1 Send Failure Handling
- Search for `try`, `catch`, `error`, `resend` in all email-sending files
- **Check**: Every `resend.emails.send()` call is wrapped in try-catch
- **Check**: Send failures are logged with context (email type, recipient, error)
- **Check**: Send failures do not crash the parent operation (order still processes)
- **Check**: Failed sends are retried (or queued for retry)
#### 5.2 Rate Limiting on Sends
- Search for `rate`, `limit`, `throttle`, `delay` in email-sending code
- **Check**: Bulk email sends (drip cron, abandoned cart cron) are rate-limited
- **Check**: Resend API rate limits are respected (check Resend docs for limits)
- **Check**: No infinite loops possible in cron handlers
#### 5.3 Bounce Handling
- Search for `bounce`, `complaint`, `webhook`, `suppression` in email-related code
- **Check**: Resend bounce webhooks are configured
- **Check**: Hard bounces remove the email from future sends
- **Check**: Complaint (spam report) suppresses future sends
- **Severity**: FAIL if bounces are ignored — harms sender reputation
#### 5.4 Email Preview/Test Endpoint
- Search for `preview`, `test`, `email.*test`, `send.*test` in API routes
- **Check**: Admin can preview email templates before sending
- **Check**: Admin can send test emails to a specific address
- **Check**: Test sends are logged differently from production sends
#### 5.5 Idempotency
- Read drip cron and abandoned cart cron logic
- **Check**: Drip cron does not re-send already-sent emails (checks `sent_at`)
- **Check**: Abandoned cart does not re-send to users who already received recovery email
- **Check**: Cron jobs are idempotent (safe to run multiple times)
#### 5.6 Email Queue Monitoring
- Search for queue, pending, stuck, failed in drip and email code
- **Check**: Is there visibility into the drip queue (admin dashboard or logs)?
- **Check**: Stuck/failed queue items are surfaced
- **Check**: Queue has a TTL (old unsent items expire)
### Phase 6: Deliverability & Infrastructure (5 checks)
#### 6.1 SPF/DKIM/DMARC
- Check for DNS configuration references, domain verification
- **Check**: SPF record includes Resend's sending IPs
- **Check**: DKIM is configured (Resend provides this)
- **Check**: DMARC policy is set (at minimum p=none for monitoring)
- **Note**: These are DNS-level checks — audit can only verify configuration references in code/docs
#### 6.2 Sending Domain
- Read from address configuration in email code
- **Check**: Emails are sent from a custom domain (not @resend.dev)
- **Check**: Domain matches the store domain (skapara.com or subdomain)
- **Check**: No-reply address is used for transactional, monitored address for support
#### 6.3 Resend API Configuration
- Search for `RESEND_API_KEY`, `Resend` initialization in code
- **Check**: API key is in environment variable (not hardcoded)
- **Check**: Resend client is initialized once (singleton pattern)
- **Check**: API key has appropriate permissions (not overly broad)
#### 6.4 Email Analytics
- Search for `track`, `open`, `click`, `analytics` in email code
- **Check**: Open tracking is enabled (via Resend or tracking pixel)
- **Check**: Click tracking is enabled for CTAs
- **Check**: Analytics data is accessible (Resend dashboard or custom)
#### 6.5 Suppression List
- Search for `suppress`, `blacklist`, `block`, `opt_out` in email code
- **Check**: Unsubscribed users are added to a suppression list
- **Check**: Suppression list is checked before every send
- **Check**: Suppression persists across email types (unsubscribe from drip also suppresses abandoned cart)
---
## Output Format
Generate `AUDIT_EMAIL_TEMPLATES_[DATE].md` at the workspace root with the following structure:
```markdown
# Email Templates & Deliverability Audit — [DATE]
## Executive Summary
[2-3 paragraph overview: compliance status, template quality, deliverability posture. State risk level for EU store operation.]
## Scorecard
| Category | Checks | Pass | Warn | Fail | Critical | Score |
|---|---|---|---|---|---|---|
| CAN-SPAM & Legal Compliance | 10 | X | X | X | X | X% |
| HTML Email Structure | 8 | X | X | X | X | X% |
| Internationalization | 6 | X | X | X | X | X% |
| Brand Consistency | 5 | X | X | X | X | X% |
| Error Handling & Reliability | 6 | X | X | X | X | X% |
| Deliverability & Infrastructure | 5 | X | X | X | X | X% |
| **TOTAL** | **40** | **X** | **X** | **X** | **X** | **X%** |
## Findings
| ID | Severity | Category | Finding | File:Line | Recommendation |
|---|---|---|---|---|---|
| EM-001 | CRITICAL | CAN-SPAM | [description] | `src/app/api/cron/abandoned-cart-recovery/route.ts:XX` | [fix] |
| EM-002 | FAIL | HTML Structure | [description] | `src/lib/resend.ts:XX` | [fix] |
| EM-003 | WARN | Brand | [description] | `src/lib/resend.ts:XX` | [fix] |
| ... | ... | ... | ... | ... | ... |
## Critical Blockers (Must Fix Before Production)
1. [EM-001] — [description and fix]
2. ...
## High Priority (Fix Within Sprint)
1. [EM-0XX] — [description]
2. ...
## Warnings (Fix Before V2)
1. [EM-0XX] — [description]
2. ...
## Email Inventory
| Email Type | Function/File | Locale Support | Unsubscribe | RFC 8058 | CAN-SPAM Status |
|---|---|---|---|---|---|
| Order Confirmation | resend.ts:sendOrderConfirmationEmail | en/es/de | No | No | [status] |
| Order Shipped | resend.ts:sendOrderShippedEmail | en/es/de | No | No | [status] |
| Order Cancelled | resend.ts:sendOrderCancelledEmail | en/es/de | No | No | [status] |
| Order Delivered | resend.ts:sendOrderDeliveredEmail | en/es/de | No | No | [status] |
| Order Failed | resend.ts:sendOrderFailedEmail | en/es/de | No | No | [status] |
| Credit Purchase | resend.ts:sendCreditPurchaseEmail | en/es/de | No | No | [status] |
| Drip: Welcome | cron/drip/route.ts | [check] | Yes | Yes | [status] |
| Drip: Tips | cron/drip/route.ts | [check] | Yes | Yes | [status] |
| Drip: Credit Offer | cron/drip/route.ts | [check] | Yes | Yes | [status] |
| Cart Recovery 1h | cron/abandoned-cart-recovery | [check] | No | No | [status] |
| Cart Recovery 24h | cron/abandoned-cart-recovery | [check] | No | No | [status] |
| Newsletter Confirm | newsletter/subscribe | [check] | N/A | N/A | [status] |
## Priority Actions
1. [Numbered list of fixes ordered by severity and effort]
```
## Severity Levels
- **CRITICAL**: Legal compliance violation (CAN-SPAM, GDPR), email deliverability risk (no SPF/DKIM), security issue. Blocks production launch.
- **FAIL**: Broken functionality, missing i18n, rendering issues in major clients. Must fix before release.
- **WARN**: Sub-optimal pattern, missing best practice, minor UX gap. Should fix before V2.
- **PASS**: Meets standards and best practices.
## Notes
- Always verify findings against actual code (read the file, cite the line number). Never report speculative issues.
- CAN-SPAM violations carry fines up to $50,120 per email. GDPR violations up to 4% of annual revenue.
- For EU stores (SKAPARA), GDPR consent requirements are stricter than CAN-SPAM.
- Test email rendering mentally against: Gmail (web + mobile), Outlook (desktop + web), Apple Mail, Yahoo Mail.
- RFC 8058 one-click unsubscribe is required by Gmail/Yahoo for bulk senders since February 2024.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!