Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/plans/2026-07-24-luxury-verification-email-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Luxury verification email design

## Goal

Turn the registration verification email into a trustworthy brand touchpoint
without weakening deliverability, accessibility, security, or support for older
mail clients.

## Visual direction

The selected direction is **Eastern academy meets international editorial**.
It reuses the product palette instead of introducing a separate email brand:

- warm ivory `#fffdf7` and parchment `#f1eee5` for the canvas;
- ink teal `#172c35` for the main panel and typography;
- cinnabar `#bf3d2f` for the verification action;
- restrained champagne gold `#c9a86a` for dividers and small details.

The memorable element is a six-digit code presented like a contemporary
digital seal. Generous whitespace, one display-serif heading, precise
bilingual microcopy, and thin ornamental rules provide the premium character.
There are no stock illustrations, gradients, tracking pixels, remote fonts, or
generic app-style cards.

## Information hierarchy

1. Hidden preheader: explains that this is a time-sensitive registration step
without exposing the code on a lock screen.
2. Brand masthead: Chinese platform name, small English descriptor, and a
typographic seal.
3. Main message: concise welcome and one instruction.
4. Verification panel: six-digit code, ten-minute duration, and exact expiry in
Asia/Shanghai.
5. Security note: never share the code; ignore the email when unrequested.
6. Quiet footer: transactional-email explanation and copyright line.

## Architecture and compatibility

`createVerificationMessage` remains the only renderer. The HMAC relay payload,
SMTP transport configuration, API validation, and registration flow do not
change. The email uses presentation tables and inline styles because Outlook
desktop still relies on a Word-based renderer. A small mobile media query is
progressive enhancement; the base layout remains usable when style blocks are
removed.

Dynamic values are HTML-escaped even though the API already restricts the code
to six digits. The HTML contains no remote image, script, form, or CSS URL. A
complete plain-text alternative remains mandatory.

## Acceptance criteria

- Gmail accepts the production message and the API returns no development code.
- The message is readable at 320 px and 640 px widths.
- The code is prominent, selectable, and present in both HTML and plain text.
- Content remains understandable with images disabled and CSS partially
stripped.
- Automated tests protect subject, expiry, escaping, structure, and the absence
of remote assets.
102 changes: 102 additions & 0 deletions docs/plans/2026-07-24-luxury-verification-email.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Luxury Verification Email Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Ship a premium, responsive registration-code email that preserves the
existing secure relay and broad email-client compatibility.

**Architecture:** Keep rendering in `createVerificationMessage` and leave the
Railway-to-Vercel HMAC contract unchanged. Build a self-contained, table-based
HTML document with inline styles, a plain-text fallback, HTML escaping, and no
remote assets.

**Tech Stack:** Node.js, Nodemailer, semantic HTML email, inline CSS, Node test
runner, Prettier, Vercel Functions.

---

### Task 1: Lock the message contract with tests

**Files:**

- Modify: `server/test/mail-provider.test.js`
- Modify: `server/test/mail-relay.test.js`

**Step 1:** Import `createVerificationMessage` and add assertions for the
bilingual masthead, code, Shanghai expiry, presentation-table structure,
preheader, security copy, and text alternative.

**Step 2:** Add a defensive escaping case that passes HTML metacharacters
directly to the renderer and verifies that executable markup never appears.

**Step 3:** Run:

```bash
pnpm exec node --test server/test/mail-provider.test.js server/test/mail-relay.test.js
```

Expected: the new visual-contract assertions fail before implementation while
the existing transport and HMAC tests continue to pass.

### Task 2: Implement the premium renderer

**Files:**

- Modify: `server/services/mail-provider.js`

**Step 1:** Add a small `escapeHtml` helper for renderer-owned dynamic content.

**Step 2:** Improve the plain-text message with a clear title, code, expiry,
security guidance, and support context.

**Step 3:** Replace the minimal HTML fragment with a complete email document:
hidden preheader, ink masthead, ivory body, cinnabar code panel, exact expiry,
security note, and restrained footer.

**Step 4:** Keep every critical style inline, use `role="presentation"` layout
tables, and avoid external images, fonts, scripts, forms, and CSS URLs.

**Step 5:** Re-run the two focused test files. Expected: all tests pass.

### Task 3: Validate the visual output

**Files:**

- Temporary artifact only: `C:\tmp\international-chinese-verification-email.html`
- Temporary artifact only: `C:\tmp\international-chinese-verification-email.png`

**Step 1:** Render a deterministic example using
`createVerificationMessage`.

**Step 2:** Capture the HTML at a mobile-friendly viewport and inspect the
screenshot for hierarchy, clipping, contrast, spacing, and code legibility.

**Step 3:** Fix any visible issue before continuing and delete temporary
preview artifacts after review.

### Task 4: Run repository verification

**Files:** No new files.

**Step 1:** Run `pnpm lint:check`.

**Step 2:** Run `pnpm format:check`.

**Step 3:** Run `pnpm test:api`.

**Step 4:** Run `pnpm build`.

Expected: every command exits successfully.

### Task 5: Release and production smoke test

**Files:** Commit the renderer, tests, and design documents.

**Step 1:** Commit and push `codex/luxury-verification-email`.

**Step 2:** Merge only after GitHub CI and Vercel preview checks pass.

**Step 3:** Confirm Railway and Vercel production deployments are healthy.

**Step 4:** Request a production verification code through the public API and
confirm HTTP 200, an expiry timestamp, and no `developmentCode`.
206 changes: 195 additions & 11 deletions server/services/mail-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,211 @@ import { createHmac } from 'node:crypto'

import nodemailer from 'nodemailer'

const HTML_ENTITIES = Object.freeze({
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})

function escapeHtml(value) {
return String(value).replace(
/[&<>"']/g,
(character) => HTML_ENTITIES[character]
)
}

function createVerificationMessage({ email, code, expiresAt, mailFrom }) {
const expiryDate = new Date(expiresAt)
if (Number.isNaN(expiryDate.getTime())) {
throw new TypeError('Verification code expiry is invalid')
}
const expiry = new Intl.DateTimeFormat('zh-CN', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Asia/Shanghai'
}).format(new Date(expiresAt))
}).format(expiryDate)
const expiryWithZone = `${expiry}(北京时间)`
const safeCode = escapeHtml(code)
const safeExpiry = escapeHtml(expiryWithZone)

return {
from: mailFrom,
to: email,
subject: '国际中文学习平台注册验证码',
text: `你的注册验证码是 ${code},有效期至 ${expiry}。请勿向任何人透露此验证码。`,
html: `
<main style="max-width:560px;margin:auto;padding:32px;color:#172c35;font-family:serif">
<p style="color:#bf3d2f;letter-spacing:.12em">INTERNATIONAL CHINESE PLATFORM</p>
<h1 style="font-size:28px">完成你的平台注册</h1>
<p>请在注册页面输入下面的六位验证码:</p>
<p style="font:700 34px monospace;letter-spacing:.22em">${code}</p>
<p style="color:#66757b">验证码有效期至 ${expiry}。如果这不是你的操作,请忽略本邮件。</p>
</main>
`
text: `国际中文教育平台
INTERNATIONAL CHINESE EDUCATION

完成你的平台注册

你的注册验证码是:
${code}

有效期 10 分钟,至 ${expiryWithZone}。

请勿向任何人透露此验证码。平台工作人员不会向你索取验证码。
如果这不是你的操作,请忽略本邮件,无需进行任何处理。

这是一封由系统自动发送的事务邮件,请勿直接回复。`,
headers: {
'Auto-Submitted': 'auto-generated',
'X-Auto-Response-Suppress': 'All'
},
html: `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
<title>国际中文学习平台注册验证码</title>
<style>
@media only screen and (max-width: 620px) {
.email-shell { width: 100% !important; }
.mobile-padding { padding-left: 24px !important; padding-right: 24px !important; }
.brand-title { font-size: 22px !important; }
.message-title { font-size: 30px !important; line-height: 38px !important; }
.verification-code { font-size: 36px !important; letter-spacing: 7px !important; }
.meta-column { display: block !important; width: 100% !important; box-sizing: border-box !important; }
.meta-column-first { padding: 0 0 16px !important; }
.meta-column-last { padding: 16px 0 0 !important; border-left: 0 !important; border-top: 1px solid #d7d0bf !important; }
}
</style>
</head>
<body style="margin:0;padding:0;background-color:#f1eee5;color:#172c35;">
<div style="display:none;font-size:1px;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;mso-hide:all;">
你的注册验证码将在 10 分钟后失效,请及时完成验证。
</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f1eee5" style="width:100%;border-collapse:collapse;background-color:#f1eee5;">
<tr>
<td align="center" style="padding:36px 12px;">
<!--[if mso]>
<table role="presentation" width="600" align="center" cellpadding="0" cellspacing="0" border="0">
<tr>
<td>
<![endif]-->
<table class="email-shell" role="presentation" width="600" align="center" cellpadding="0" cellspacing="0" border="0" bgcolor="#fffdf7" style="width:100%;max-width:600px;border-collapse:separate;background-color:#fffdf7;border:1px solid #d7d0bf;">
<tr>
<td class="mobile-padding" bgcolor="#172c35" style="padding:34px 40px 30px;background-color:#172c35;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;">
<tr>
<td valign="middle" style="padding:0;">
<p style="margin:0 0 7px;color:#c9a86a;font-family:Consolas,Menlo,'Courier New',monospace;font-size:10px;line-height:15px;font-weight:700;letter-spacing:2.1px;text-transform:uppercase;mso-line-height-rule:exactly;">
INTERNATIONAL CHINESE EDUCATION
</p>
<p class="brand-title" style="margin:0;color:#fffdf7;font-family:Georgia,'Songti SC',STSong,serif;font-size:24px;line-height:32px;font-weight:700;letter-spacing:1px;mso-line-height-rule:exactly;">
国际中文教育平台
</p>
</td>
<td width="58" valign="middle" align="right" style="width:58px;padding:0 0 0 12px;">
<table role="presentation" width="48" cellpadding="0" cellspacing="0" border="0" align="right" bgcolor="#bf3d2f" style="width:48px;border-collapse:separate;background-color:#bf3d2f;border:1px solid #d98273;">
<tr>
<td width="48" height="48" align="center" valign="middle" style="width:48px;height:48px;color:#fffdf7;font-family:Georgia,'Songti SC',STSong,serif;font-size:24px;line-height:48px;font-weight:700;mso-line-height-rule:exactly;">
</td>
</tr>
</table>
</td>
</tr>
</table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;">
<tr>
<td height="1" bgcolor="#c9a86a" style="height:1px;padding:0;background-color:#c9a86a;font-size:0;line-height:0;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="mobile-padding" bgcolor="#fffdf7" style="padding:48px 40px 20px;background-color:#fffdf7;">
<p style="margin:0 0 15px;color:#bf3d2f;font-family:Consolas,Menlo,'Courier New',monospace;font-size:11px;line-height:17px;font-weight:700;letter-spacing:1.8px;text-transform:uppercase;mso-line-height-rule:exactly;">
REGISTRATION VERIFICATION
</p>
<h1 class="message-title" style="margin:0 0 19px;color:#172c35;font-family:Georgia,'Songti SC',STSong,serif;font-size:36px;line-height:46px;font-weight:700;letter-spacing:.2px;mso-line-height-rule:exactly;">
完成你的平台注册
</h1>
<p style="margin:0;color:#5a696e;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif;font-size:16px;line-height:26px;font-weight:400;mso-line-height-rule:exactly;">
欢迎来到国际中文教育平台。请在注册页面输入下面的六位验证码,完成身份确认。
</p>
</td>
</tr>
<tr>
<td class="mobile-padding" bgcolor="#fffdf7" style="padding:20px 40px 24px;background-color:#fffdf7;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#bf3d2f" style="width:100%;border-collapse:separate;background-color:#bf3d2f;border:1px solid #a93125;">
<tr>
<td align="center" style="padding:24px 20px 8px;color:#f7d7cf;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif;font-size:12px;line-height:18px;font-weight:700;letter-spacing:1.5px;mso-line-height-rule:exactly;">
你的验证码
</td>
</tr>
<tr>
<td class="verification-code" dir="ltr" align="center" style="padding:0 12px 26px;color:#ffffff;font-family:Consolas,Menlo,'Courier New',monospace;font-size:42px;line-height:52px;font-weight:700;letter-spacing:10px;white-space:nowrap;mso-line-height-rule:exactly;">
${safeCode}
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="mobile-padding" bgcolor="#fffdf7" style="padding:0 40px 36px;background-color:#fffdf7;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;">
<tr>
<td class="meta-column meta-column-first" width="50%" valign="top" style="width:50%;padding:0 12px 0 0;">
<p style="margin:0 0 5px;color:#8a6d3b;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif;font-size:11px;line-height:17px;font-weight:700;letter-spacing:1px;mso-line-height-rule:exactly;">
有效期 10 分钟
</p>
<p style="margin:0;color:#5a696e;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif;font-size:13px;line-height:21px;font-weight:400;mso-line-height-rule:exactly;">
${safeExpiry}
</p>
</td>
<td class="meta-column meta-column-last" width="50%" valign="top" style="width:50%;padding:0 0 0 12px;border-left:1px solid #d7d0bf;">
<p style="margin:0 0 5px;color:#8a6d3b;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif;font-size:11px;line-height:17px;font-weight:700;letter-spacing:1px;mso-line-height-rule:exactly;">
使用方式
</p>
<p style="margin:0;color:#5a696e;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif;font-size:13px;line-height:21px;font-weight:400;mso-line-height-rule:exactly;">
返回注册页面,输入上方验证码。
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="mobile-padding" bgcolor="#fffdf7" style="padding:0 40px 42px;background-color:#fffdf7;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f6f1e7" style="width:100%;border-collapse:separate;background-color:#f6f1e7;border-left:3px solid #c9a86a;">
<tr>
<td style="padding:18px 20px;">
<p style="margin:0 0 5px;color:#172c35;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif;font-size:13px;line-height:20px;font-weight:700;mso-line-height-rule:exactly;">
安全提示
</p>
<p style="margin:0;color:#66757b;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif;font-size:13px;line-height:21px;font-weight:400;mso-line-height-rule:exactly;">
请勿向任何人透露验证码。平台工作人员不会向你索取验证码;如果这不是你的操作,请忽略本邮件。
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="mobile-padding" bgcolor="#172c35" style="padding:24px 40px;background-color:#172c35;border-top:1px solid #c9a86a;">
<p style="margin:0 0 5px;color:#d7d0bf;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif;font-size:11px;line-height:18px;font-weight:400;mso-line-height-rule:exactly;">
这是一封由系统自动发送的事务邮件,请勿直接回复。
</p>
<p style="margin:0;color:#8f9a9d;font-family:Consolas,Menlo,'Courier New',monospace;font-size:10px;line-height:16px;font-weight:400;letter-spacing:.7px;mso-line-height-rule:exactly;">
INTERNATIONAL CHINESE EDUCATION · LEARN · CONNECT · GROW
</p>
</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</body>
</html>`
}
}

Expand Down
Loading
Loading