阿里云邮件推送验证邮箱
本章用阿里云邮件推送(DirectMail)完成邮箱验证的发信环节,覆盖控制台准备、SMTP 与 SDK(OpenAPI)两种发送方式,以及和注册流程的对接点。
验证方式取值见验证类型:verification_type=1 发验证码邮件,2 发验证链接邮件。这两种都是"把验证信息发给用户",发送成功不代表邮箱已验证,只有后端核验通过才能更新验证状态,核验与奖励发放见邮箱登录和注册。
本章所有域名、记录值、账号与密钥均为脱敏示例,实际值以控制台生成的为准。密钥不得写进仓库、前端、接口响应或日志。
1. 控制台准备
控制台入口:https://dm.console.aliyun.com/。账号需完成实名认证。
1.1 创建发信域名
建议用子域名(例如 notice.example.com),不要直接用主域 example.com:验证码邮件的信誉与将来的营销邮件、通知邮件互相隔离,一方退信多不会把另一方拖进垃圾箱。
添加发信域名后,控制台会给出需要到 DNS 服务商处添加的记录,通常是四类:
1)DKIM 验证(所有权与防伪造)
| 类型 | 主机记录 | 主域名 | 记录值(示例,控制台会给出完整公钥) |
|---|---|---|---|
TXT | aliyun-cn-hangzhou._domainkey.notice | example.com | v=DKIM1; k=rsa; p=<控制台生成的公钥> |
DKIM 用于证明邮件确实由该域名发出,有助于防止域名被伪造、降低被判为垃圾邮件的概率。
2)SPF 验证
| 类型 | 主机记录 | 主域名 | 记录值 |
|---|---|---|---|
TXT | notice | example.com | v=spf1 include:spf1.dm.aliyun.com -all |
如果该域名已有 SPF 记录,不要新增第二条 TXT,而是把 include:spf1.dm.aliyun.com 合并进已有的那条 SPF 值里。一个域名只能有一条 SPF 记录。
3)DMARC 验证
| 类型 | 主机记录 | 主域名 | 记录值 |
|---|---|---|---|
TXT | _dmarc.notice | example.com | v=DMARC1;p=none;rua=mailto:[email protected] |
DMARC 让收信方知道"收到伪造该域名的邮件时该怎么处理",p=none 表示先只上报、不拦截,适合刚接入时观察。
4)MX 验证(收信配置)
| 类型 | 主机记录 | 主域名 | 记录值 |
|---|---|---|---|
MX | notice | example.com | mx01.dm.aliyun.com |
这条记录要保持存在,否则会影响发信(控制台提示无法发信时优先检查它)。
DNS 记录添加后回到控制台点验证;各地 DNS 生效时间不一致,通常几分钟到几十分钟。
1.2 创建发信地址
在邮件推送控制台选择刚创建的发信域名,新建发信地址:
- 账号:
noreply(按需改成其它前缀) - 密码:自己设置,这就是发信地址的 SMTP 密码,与阿里云账号密码无关
创建后得到一个完整发信地址,例如 [email protected],它同时是后面两种发送方式的身份:
- SMTP 方式:
用户名 = 发信地址全称,密码 = 这里设置的 SMTP 密码; - SDK/API 方式:作为接口参数
AccountName,不使用 SMTP 密码,改用 AccessKey 鉴权。
1.3 控制台发信与限额
控制台、SMTP、API 三种入口发送的都是同一套信誉与额度:
- 批量营销邮件走控制台或
BatchSendMail(需要已审核的模板与收件人列表); - 单条触发邮件(验证码、验证链接)走 SMTP 或
SingleSendMail; - 新账号有信誉等级限制每日发信量,靠低退信率逐级提额,正式放量前先自测。
2. 配置与依赖
配置集中放在部署环境的配置文件或不提交的 .env 中:
# 发送方式:aliyun 走 SMTP,aliyun-sdk 走 OpenAPI(本章两种都可用)
MAIL_PROVIDER=aliyun
# SMTP 方式
MAIL_HOST=smtpdm.aliyun.com
MAIL_PORT=465
MAIL_ACCOUNT=[email protected]
MAIL_PASSWORD=
MAIL_FROM_ALIAS=Example App
# SDK / API 方式
ALIBABA_CLOUD_ACCESS_KEY_ID=
ALIBABA_CLOUD_ACCESS_KEY_SECRET=
MAIL_DM_ENDPOINT=dm.aliyuncs.com
MAIL_DM_REGION=cn-hangzhou
读取配置用 EnvUtils.getStr(...),密钥不要用 System.getenv 散落在业务代码里。独立程序在读取前调用 EnvUtils.load();框架启动时已加载环境,不需要重复调用。
依赖坐标(版本以官方文档为准,下面不写死版本号):
<!-- SDK / API 方式:阿里云 OpenAPI 核心 SDK,用 CommonRequest 调用邮件推送接口 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>以官方文档为准</version>
</dependency>
<!-- SMTP 方式:JavaMail -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>以官方文档为准</version>
</dependency>
如果改用邮件推送的产品 SDK(alibabacloud-dm20151123),依赖以 SDK 文档给出的坐标与版本为准;它内部基于网关异步调用,用法与下面的 CommonRequest 不同。
3. SMTP 方式发送
[email protected] 建好后即可用 SMTP 发信。要点:
- 服务器
smtpdm.aliyun.com; - 端口
465走隐式 SSL(推荐),587走 STARTTLS,25常被云厂商封禁; - 用户名是发信地址全称,密码是控制台为这个地址设置的 SMTP 密码;
From用发信地址本身,不要用裸的no-reply之类没有域名的值。
Properties props = new Properties();
props.setProperty("mail.transport.protocol", "smtp");
props.setProperty("mail.smtp.host", EnvUtils.getStr("MAIL_HOST"));
props.setProperty("mail.smtp.port", EnvUtils.getStr("MAIL_PORT"));
props.setProperty("mail.smtp.auth", "true");
props.setProperty("mail.smtp.ssl.enable", "true"); // 465 走 SSL;587 改成 starttls.enable
props.setProperty("mail.smtp.connectiontimeout", "5000"); // 发信不能长时间阻塞请求线程
props.setProperty("mail.smtp.timeout", "10000");
props.setProperty("mail.smtp.writetimeout", "10000");
String account = EnvUtils.getStr("MAIL_ACCOUNT");
String password = EnvUtils.getStr("MAIL_PASSWORD");
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(account, password);
}
});
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(account, EnvUtils.getStr("MAIL_FROM_ALIAS"), "UTF-8"));
message.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject(subject, "UTF-8");
message.setContent(html, "text/html; charset=UTF-8");
Transport.send(message);
SMTP 方式的优点是配置直观、不需要 AccessKey;缺点是拿不到 RequestId,排障时只能依赖 SMTP 返回码,也更容易被"账号迁移"这类问题掩盖(见 5.1 的说明)。
4. SDK / OpenAPI 方式发送
邮件推送的 OpenAPI 版本是 2015-11-23,单条发信接口是 SingleSendMail,域名 dm.aliyuncs.com。单次请求最多 100 个收件地址,验证码场景一次发一个即可。
官方 SDK 提供两种调用方式:产品 SDK 与 CommonRequest(泛化调用)。用 CommonRequest 不需要额外引入产品 SDK,且是同步调用,适合放在现有 EmailSender 实现里:
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysProtocol(ProtocolType.HTTPS);
request.setSysDomain("dm.aliyuncs.com");
request.setSysVersion("2015-11-23");
request.setSysAction("SingleSendMail");
request.setSysConnectTimeout(5000);
request.setSysReadTimeout(10000);
// 参数放在 Body 提交,必须声明 Body 格式,否则 SDK 直接抛 "Content type may not be null"
request.setHttpContentType(FormatType.FORM);
request.putBodyParameter("AccountName", "[email protected]"); // 发信地址
request.putBodyParameter("AddressType", "1"); // 1 使用发信地址发信
request.putBodyParameter("ReplyToAddress", "false"); // 是否需要回信地址
request.putBodyParameter("ToAddress", to); // 多个收件人用逗号分隔,最多 100 个
request.putBodyParameter("Subject", subject);
request.putBodyParameter("HtmlBody", html);
request.putBodyParameter("FromAlias", "Example App"); // 收件人看到的发信人昵称
// 可选:邮件标签,配了才能在控制台按标签查看发信统计
request.putBodyParameter("TagName", "verify-code");
IAcsClient client = new DefaultAcsClient(DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret));
CommonResponse response = client.getCommonResponse(request);
if (response.getHttpStatus() != 200) {
throw new IllegalStateException("send mail failed, body:" + response.getData());
}
三个容易踩的点:
- 必须设置 Body 格式(
FormatType.FORM),否则还没发出请求就会抛Content type may not be null; - 参数走 Body 而不是查询串:验证码邮件的 HTML 正文可能很长,放 URL 里容易被长度限制拦截;
- 鉴权用 AccessKey,不是发信地址的 SMTP 密码。官方也支持用环境变量
ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET提供,代码从环境变量读取,不要硬编码。
排障时把接口返回的错误码原样带进异常信息:
} catch (ClientException e) {
// 例:code:InvalidMailAddress.NotFound, msg:The specified mail address is not found., requestId:xxxx
throw new IllegalStateException("send mail failed, code:" + e.getErrCode() + ", msg:" + e.getErrMsg()
+ ", requestId:" + e.getRequestId(), e);
}
5. 与注册流程对接
框架的发信入口是 EmailSender 接口,注册与重置密码流程会自动调用它,实现类需要在启动时注册到 TioBootServer:
public class AliyunDmEmailSender implements EmailSender {
@Override
public boolean send(String to, String subject, String content) {
// 普通通知邮件,SMTP 或 SDK 任选一种实现
}
@Override
public boolean sendVerificationCodeEmail(String to, String origin, String code) {
// verification_type=1:把 6 位验证码填进模板后发送
}
@Override
public boolean sendVerificationEmail(String to, String origin, String code) {
// verification_type=2:把 origin + "/verification/email?email=..&code=.." 拼成链接后发送
}
}
// 启动配置里注册,否则框架取不到发信实现
TioBootServer.me().setEmailSender(new AliyunDmEmailSender());
模板渲染、验证码记录表、核验接口与重发限制见邮箱登录和注册;那章第 8 节讲的是框架侧的 EmailSender 与模板,本章讲的是"用阿里云把信真正发出去"。
几个业务上的要点:
- 发送成功不等于验证完成:只有核验通过才更新
app_users.email_verified之类的验证状态; - 别把敏感动作放在注册成功时:注册即可发放的奖励、邀请返利等,建议延后到邮箱验证通过后再发放,否则可以用不存在或不属于自己的邮箱批量注册刷奖励;
- 重发限流:发信接口通常是免登录放行的,按邮箱做重发间隔(例如 60 秒)与每日上限,避免被刷验证码邮件;
- 验证码有效期:签发时写入过期时间,核验时同时判断"未使用 + 未过期"。
5.1 SMTP 能发不代表 API 能发
两条通道的鉴权维度不同:
- SMTP 只认"发信地址 + SMTP 密码",地址配好就能发;
- OpenAPI 是按 AccessKey 所属账号隔离配置的。
所以用 A 账号的发信地址配 SMTP、却用 B 账号的 AccessKey 调 API,会出现"SMTP 正常、API 报发信地址不存在/用户不存在"的现象。AccessKey 必须来自已开通邮件推送的那个账号,建议单独建只授予邮件推送权限的 RAM 子账号。
6. 常见错误与排查
| 现象 / 错误码 | 原因与处理 |
|---|---|
InvalidUser.NotFound | AccessKey 所属账号没有开通邮件推送,或 AccessKey 与邮件推送配置不在同一账号 |
InvalidMailAddress.NotFound | 发信地址不存在:控制台还没建该地址,或发信域名未验证通过 |
InvalidAccountName | 发信地址格式不对,必须是完整邮箱 |
Forbidden / NoPermission | 当前 AccessKey 没有邮件推送权限,给对应 RAM 用户授权 |
InvalidAccessKeyId.NotFound | AccessKey 错误或已被禁用 |
SignatureDoesNotMatch | AccessKey Secret 错误,或服务器时间与北京时间相差超过 15 分钟 |
| 发信量被限制 | 新账号信誉等级(每日发信量)限制,控制台可查看当前等级与额度 |
SDK 抛 Content type may not be null | 用 Body 传参时没有设置 Body 格式,补上 FormatType.FORM |
| SMTP 认证失败 | 用户名不是发信地址全称,或用了账号密码而不是发信地址的 SMTP 密码 |
| 收件人收不到、进了垃圾箱 | 检查 DKIM/SPF/DMARC 是否生效、是否用了主域发信、正文是否像营销内容 |
排查顺序建议:先用控制台的"发送测试"确认账号与地址没问题,再用代码发一封给自己,最后才接入注册流程。代码侧每次发信都记录收件人、主题与接口返回的 RequestId(不要记录验证码明文与密钥),便于对账。
7. 上线检查清单
- 发信域名四项 DNS 记录(DKIM、SPF、DMARC、MX)都已验证通过,且 MX 记录保持存在;
- 发信域名用的是子域名,与主域隔离;
- 发信地址已创建,SMTP 密码只存在部署环境,未进仓库;
- 走 SDK/API 时,AccessKey 来自已开通邮件推送的账号,且只授予必要权限;
- 真实发一封验证码邮件确认能收到、模板渲染正确、链接或验证码可用;
- 已确认当前信誉等级的每日发信量,并配置了重发限流;
- 邮件里的品牌名、站点地址、logo 图片等外部资源地址都是正式域名(图片地址若指向接口域名,确认该接口可公网访问)。
