关于 ship-email
面向开发者的编码技能,用于为现有应用搭建邮件基础设施,包括 Resend 配置、事务邮件模板、用户分组和管理端群发界面。它会先检查项目的技术栈、数据库和身份验证方式,以适配现有应用。
主要功能
- 01检查框架、数据库和身份验证
- 02配置 Resend 与共享邮件工具
- 03创建事务邮件模板
- 04按用户分组发送活动邮件
- 05构建带身份验证的管理端发送界面
- 06添加退订处理和验证步骤
适用场景
- 01添加注册欢迎邮件等事务邮件
- 02向指定用户群组发送活动邮件
- 03避免邮件故障中断应用流程
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tushaarmehtaa/ tushar-skills --skill ship-email安装后在 Claude Code 中输入/ship-email确认已加载。
附带文件references/guide.md
详细介绍
搭建完整的邮件基础设施——Resend 配置、事务性模板、用户细分和管理员发送界面。先读取项目,再接入现有的身份验证和数据库。
Resend 集成中三个容易被忽略的问题
- 上线前未验证域名。 Resend 默认使用
@resend.dev发送邮件。邮件可能进入垃圾邮件箱,或被完全拦截。请在 Resend 控制面板中验证域名,并在向真实用户发送邮件前确认from地址与已验证的域名匹配。 - 邮件发送失败时抛出异常。 如果
sendEmail抛出异常且未捕获,欢迎邮件发送失败就会导致注册流程崩溃。邮件发送属于即发即弃操作——务必捕获异常,务必返回{ success, error },绝不抛出异常。 - 使用
noreply地址发送邮件,却未设置reply-to。 用户有问题时会点击回复。如果收件端没有人查看的收件箱,你就会失去这些用户。即使from是noreply@,也要将replyTo设置为真实的收件箱地址。
阶段 1:检测项目
开始编写前,先读取代码库:
1.1 技术栈检测
- 框架:Next.js / Express / FastAPI / 其他?
- 数据库:Supabase / Prisma / Drizzle / Mongoose / 原始 SQL?
- 身份验证:哪些字段用于识别用户?(
clerk_user_id、email、id) - 用户表结构:有哪些字段?(
email、name、credits、plan、created_at、last_active_at)
1.2 询问用户
搭建前,先确认:
I'll wire email for your [framework] app with [database].
Quick decisions:
1. What emails do you need? (transactional, campaigns, or both)
2. Sender: From name and from email? (e.g., "Tushar from Bangers Only <[email protected]>")
3. Domain verified in Resend? (yes / need to set it up)
Defaults: welcome email + re-engagement campaign, Resend.
阶段 2:服务提供商配置
安装并配置 Resend
npm install resend
添加到 .env.example:
RESEND_API_KEY=re_xxxxx
创建邮件工具——代码库中的所有邮件都通过它发送:
// lib/email.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
interface SendEmailParams {
to: string | string[];
subject: string;
html: string;
from?: string;
replyTo?: string;
}
export async function sendEmail({
to,
subject,
html,
from = 'Your Name <[email protected]>',
replyTo,
}: SendEmailParams) {
try {
const result = await resend.emails.send({ from, to, subject, html, reply_to: replyTo });
return { success: true, id: result.data?.id };
} catch (error) {
console.error('Email send failed:', error);
return { success: false, error };
}
}
邮件工具绝不能抛出异常。 邮件发送失败不应导致主请求流程崩溃。返回 { success, error },由调用方决定如何处理。
域名验证检查清单
告知用户在正式上线前,于 Resend 控制面板中完成以下操作:
[ ] Add MX record to DNS
[ ] Add SPF TXT record: v=spf1 include:resend.com ~all
[ ] Add DKIM TXT records (Resend provides these in dashboard)
[ ] Verify domain status shows "Verified" in Resend dashboard
[ ] Set reply-to to a monitored inbox (not noreply)
如果未验证域名,邮件可能进入垃圾邮件箱,或根本无法发送。
阶段 3:邮件模板
根据代码库中已有的事件创建模板。至少搭建以下模板:
欢迎邮件(用户注册时触发)
// lib/emails/welcome.ts
export function welcomeEmail({ name, ctaUrl }: { name: string; ctaUrl: string }): string {
return `
<div style="font-family: sans-serif; max-width: 560px; margin: 0 auto; padding: 40px 20px; color: #111;">
<p>Hey ${name},</p>
<p>You're in. Here's what to do first:</p>
<p>
<a href="${ctaUrl}"
style="display: inline-block; background: #000; color: #fff; padding: 12px 24px;
text-decoration: none; border-radius: 6px; font-size: 14px;">
Get started
</a>
</p>
<p>Any questions — just reply to this email.</p>
<p style="color: #666; font-size: 13px; margin-top: 40px;">— [Your Name]</p>
<p style="color: #999; font-size: 11px;">
<a href="{{{unsubscribe_url}}}" style="color: #999;">Unsubscribe</a>
</p>
</div>
`;
}
重新互动邮件(针对 7 天以上未活跃的用户触发)
// lib/emails/reengagement.ts
export function reengagementEmail({ name, daysSinceActive }: { name: string; daysSinceActive: number }): string {
return `
<div style="font-family: sans-serif; max-width: 560px; margin: 0 auto; padding: 40px 20px; color: #111;">
<p>Hey ${name},</p>
<p>Haven't seen you in ${daysSinceActive} days. [Product] has [one improvement since they last used it].</p>
<p>
<a href="[APP_URL]"
style="display: inline-block; background: #000; color: #fff; padding: 12px 24px;
text-decoration: none; border-radius: 6px; font-size: 14px;">
Pick up where you left off
</a>
</p>
<p style="color: #666; font-size: 13px; margin-top: 40px;">— [Your Name]</p>
<p style="color: #999; font-size: 11px;">
<a href="{{{unsubscribe_url}}}" style="color: #999;">Unsubscribe</a>
</p>
</div>
`;
}
模板规则
- 使用纯文本风格,不要设计复杂的 HTML
- 每封邮件只包含一个 CTA
- 由真人署名,不要使用“团队”作为署名
- 每个营销活动模板都必须包含退订链接
- 仅使用内联样式,不要使用外部样式表(垃圾邮件过滤器会移除它们)
将欢迎邮件接入注册流程
找到注册 / 身份验证同步端点,并在创建用户后调用 sendEmail:
// After creating the new user:
if (isNewUser) {
await sendEmail({
to: user.email,
subject: 'Welcome',
html: welcomeEmail({ name: user.name || 'there', ctaUrl: process.env.APP_URL! }),
});
}
阶段 4:用户细分
读取用户表结构,并根据现有字段生成用户细分:
| 如果表中有…… | 生成以下细分 |
|---|---|
credits / usage_count | 高活跃(前 20%)、活跃(中间 60%)、不活跃(后 20%) |
created_at | 新用户(<7 天)、已建立用户(7-30 天)、资深用户(30 天以上) |
plan / subscription_tier | 免费、付费、已流失 |
last_active_at | 活跃(<7 天)、休眠(7-30 天)、已流失(30 天以上) |
为存在的每个细分生成实际的 SQL / ORM 查询。基于积分的应用示例如下:
-- Dormant users: used some credits but went quiet
SELECT email, name
FROM users
WHERE (initial_credits - credits) > 0
AND last_active_at < NOW() - INTERVAL '7 days'
AND email_unsubscribed = false;
阶段 5:管理员营销活动界面
如果用户要求创建营销活动,则搭建管理员路由:
POST /api/admin/send-campaign
Body: { segment: string, templateId: string }
处理逻辑:
- 检查管理员身份验证——绝不能跳过
- 查询细分中的用户
- 循环发送邮件——逐封捕获错误,单封发送失败时不要中止整个批次
- 返回
{ sentCount, failedEmails, errors }
// api/admin/send-campaign/route.ts
export async function POST(req: Request) {
const adminSecret = req.headers.get('x-admin-secret');
if (adminSecret !== process.env.ADMIN_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const { segment, templateId } = await req.json();
const users = await getUsersInSegment(segment);
let sentCount = 0;
const failedEmails: string[] = [];
for (const user of users) {
const result = await sendEmail({
to: user.email,
subject: getSubjectForTemplate(templateId),
html: renderTemplate(templateId, user),
});
if (result.success) {
sentCount++;
} else {
failedEmails.push(user.email);
}
}
return Response.json({ sentCount, failedEmails });
}
注意速率限制:Resend 免费套餐每天可发送 100 封邮件。付费套餐起价为每月 $20,每天可发送 50,000 封。如果细分用户数超过每日限额,请添加批次延迟或提醒用户。
第 6 阶段:取消订阅机制
每封营销邮件都必须支持正常取消订阅。这不是可选项,既是法律要求,也是邮件送达率的要求。
// lib/unsubscribe.ts
import { createHmac } from 'crypto';
export function generateUnsubToken(email: string): string {
return createHmac('sha256', process.env.UNSUB_SECRET!)
.update(email)
.digest('hex')
.slice(0, 16);
}
export function unsubUrl(email: string): string {
const token = generateUnsubToken(email);
return `${process.env.APP_URL}/unsubscribe?email=${encodeURIComponent(email)}&token=${token}`;
}
如果 users 表中尚不存在该字段,请添加 email_unsubscribed boolean DEFAULT false。取消订阅路由会将其设为 true,所有细分查询都必须过滤掉 email_unsubscribed = false 的记录。
第 7 阶段:验证
Flow 1: Transactional
[ ] User signs up → welcome email arrives in inbox (not spam)
[ ] Email shows from verified domain, not @resend.dev
[ ] Reply-to is a real inbox
Flow 2: Campaigns
[ ] Admin send-campaign endpoint requires auth
[ ] Segment query returns correct users
[ ] Failed individual emails don't abort the batch
[ ] Response shows sentCount and failedEmails
Flow 3: Unsubscribe
[ ] Unsubscribe link in every campaign email
[ ] Clicking it marks user as unsubscribed in DB
[ ] Unsubscribed users excluded from future segments
Flow 4: Edge Cases
[ ] sendEmail never throws — returns { success: false } on failure
[ ] Email sending doesn't block the main signup flow
[ ] RESEND_API_KEY in .env.example (not in code)
重要说明
- 切勿将电子邮件地址硬编码。 使用环境变量设置发件人和回复地址。
- 欢迎邮件的发送时机很重要。 在后台发送,不要让注册流程等待邮件送达。
- 切换到正式 API 密钥之前,先使用 Resend 的测试模式进行测试。
- 每个事件只发送一封邮件。 不要在同一触发条件下发送多封邮件,用户会注意到。
请参阅 references/guide.md,了解按细分群体编写的邮件内容、主题行公式和速率限制模式。