定时发送:让邮件在用户时区的正确时刻到达
Rovela 的 scheduled_at 字段让邮件在指定时间才投递,并支持改时间和取消。本文演示排期、改时间、取消的完整流程,以及按用户时区计算发送时间的实战写法。

很多邮件不该在代码执行的那一刻发出去:试用到期前三天的提醒、按用户时区早上九点送达的周报、活动开始前一小时的通知。常见的做法是自己写一个定时任务,扫表、到点调用发信接口。这样能用,但要多维护一张表、一个 cron 和一套失败重试。
Rovela 的发信接口自带定时发送。请求里带上 scheduled_at,邮件会先进入「已排期」状态,到点才投递。到点之前可以改时间,也可以取消。
排一封定时邮件
scheduled_at 是 ISO 8601 时间,建议带上时区偏移或者直接用 UTC:
curl https://api.rovela.dev/emails \
-H "Authorization: Bearer $ROVELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@yourdomain.com>",
"to": "user@example.com",
"subject": "你的试用还剩 3 天",
"html": "<p>试用将在 3 天后结束,现在升级可以保留所有数据。</p>",
"scheduled_at": "2026-10-20T01:00:00Z"
}'
返回的邮件 ID 和立即发送时一样。用 GET /emails/:id 查询,状态是 scheduled。如果配置了 Webhook,会收到一个 email.scheduled 事件,到点发出后再收到 email.sent、email.delivered。
批量接口 POST /emails/batch 里的每一封都可以单独带 scheduled_at,一次请求就能排好一批不同时间的邮件。
改时间
用户改了预约、活动推迟了,用 PATCH /emails/:id 更新时间:
curl -X PATCH https://api.rovela.dev/emails/$EMAIL_ID \
-H "Authorization: Bearer $ROVELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "scheduled_at": "2026-10-21T01:00:00Z" }'
只有状态还是 scheduled 的邮件能改。已经发出的邮件会返回错误「email not found or is not scheduled」。
取消
用户在提醒发出前就已经升级了,这封信就不该再发:
curl -X POST https://api.rovela.dev/emails/$EMAIL_ID/cancel \
-H "Authorization: Bearer $ROVELA_API_KEY"
成功返回 {"object":"email","id":"...","cancelled":true}。scheduled 和还在队列里没发出的 queued 邮件都能取消,已经交给收件服务器的取消不了。
实战:按用户时区早上九点送达
服务端存的是 UTC,用户感受的是本地时间。算发送时间时把时区转换做对:
// 用户所在时区的「明天 09:00」,转成 UTC 的 ISO 字符串
function nextNineAm(timeZone) {
const now = new Date()
const local = new Date(now.toLocaleString('en-US', { timeZone }))
const offset = local.getTime() - now.getTime()
const target = new Date(local)
target.setDate(target.getDate() + 1)
target.setHours(9, 0, 0, 0)
return new Date(target.getTime() - offset).toISOString()
}
await fetch('https://api.rovela.dev/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ROVELA_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `weekly-report-${user.id}-${weekId}`,
},
body: JSON.stringify({
from: 'Acme <reports@yourdomain.com>',
to: user.email,
subject: '你的本周报告',
html: renderReport(user),
scheduled_at: nextNineAm(user.timeZone),
}),
})
几个要点:
- 把邮件 ID 存下来。取消和改时间都要用到它。比如在订阅表里加一列
reminder_email_id。 - 配合幂等键。定时任务本身也可能重跑,带上
weekly-report-<用户>-<周>这样的键,重跑时不会排出两封。 - 内容在排期时就固定了。邮件正文在请求那一刻生成,到点时不会重新渲染。像「剩余 3 天」这种会变的数字,要按发送时刻来算,或者等临近了再排。
什么时候不该用定时发送
排期适合「时间点确定、内容可以提前生成」的邮件。如果内容依赖发送那一刻的状态,比如库存、余额,还是在到点时由你的系统现查现发更稳妥。
接口细节见 发送邮件 API 文档,批量排期的写法见 批量与定时发送示例。