Skip to content

阿里云邮件推送验证邮箱 ​

本章用阿里云邮件推送(DirectMail)完成邮箱验证的发信环节,覆盖控制台准备、SMTP 与 SDK(OpenAPI)两种发送方式,以及和注册流程的对接点。

验证方式取值见验证类型:verification_type=1 发验证码邮件,2 发验证链接邮件。这两种都是"把验证信息发给用户",发送成功不代表邮箱已验证,只有后端核验通过才能更新验证状态,核验与奖励发放见邮箱登录和注册。

本章所有域名、记录值、账号与密钥均为脱敏示例,实际值以控制台生成的为准。密钥不得写进仓库、前端、接口响应或日志。

1. 控制台准备 ​

控制台入口:https://dm.console.aliyun.com/。账号需完成实名认证。

1.1 创建发信域名 ​

建议用子域名(例如 notice.example.com),不要直接用主域 example.com:验证码邮件的信誉与将来的营销邮件、通知邮件互相隔离,一方退信多不会把另一方拖进垃圾箱。

添加发信域名后,控制台会给出需要到 DNS 服务商处添加的记录,通常是四类:

1)DKIM 验证(所有权与防伪造)

类型主机记录主域名记录值(示例,控制台会给出完整公钥)
TXTaliyun-cn-hangzhou._domainkey.noticeexample.comv=DKIM1; k=rsa; p=<控制台生成的公钥>

DKIM 用于证明邮件确实由该域名发出,有助于防止域名被伪造、降低被判为垃圾邮件的概率。

2)SPF 验证

类型主机记录主域名记录值
TXTnoticeexample.comv=spf1 include:spf1.dm.aliyun.com -all

如果该域名已有 SPF 记录,不要新增第二条 TXT,而是把 include:spf1.dm.aliyun.com 合并进已有的那条 SPF 值里。一个域名只能有一条 SPF 记录。

3)DMARC 验证

类型主机记录主域名记录值
TXT_dmarc.noticeexample.comv=DMARC1;p=none;rua=mailto:dmarc_report@service.aliyun.com

DMARC 让收信方知道"收到伪造该域名的邮件时该怎么处理",p=none 表示先只上报、不拦截,适合刚接入时观察。

4)MX 验证(收信配置)

类型主机记录主域名记录值
MXnoticeexample.commx01.dm.aliyun.com

这条记录要保持存在,否则会影响发信(控制台提示无法发信时优先检查它)。

DNS 记录添加后回到控制台点验证;各地 DNS 生效时间不一致,通常几分钟到几十分钟。

1.2 创建发信地址 ​

在邮件推送控制台选择刚创建的发信域名,新建发信地址:

  • 账号:noreply(按需改成其它前缀)
  • 密码:自己设置,这就是发信地址的 SMTP 密码,与阿里云账号密码无关

创建后得到一个完整发信地址,例如 noreply@notice.example.com,它同时是后面两种发送方式的身份:

  • SMTP 方式:用户名 = 发信地址全称,密码 = 这里设置的 SMTP 密码;
  • SDK/API 方式:作为接口参数 AccountName,不使用 SMTP 密码,改用 AccessKey 鉴权。

1.3 控制台发信与限额 ​

控制台、SMTP、API 三种入口发送的都是同一套信誉与额度:

  • 批量营销邮件走控制台或 BatchSendMail(需要已审核的模板与收件人列表);
  • 单条触发邮件(验证码、验证链接)走 SMTP 或 SingleSendMail;
  • 新账号有信誉等级限制每日发信量,靠低退信率逐级提额,正式放量前先自测。

2. 配置与依赖 ​

配置集中放在部署环境的配置文件或不提交的 .env 中:

properties
# 发送方式:aliyun 走 SMTP,aliyun-sdk 走 OpenAPI(本章两种都可用)
MAIL_PROVIDER=aliyun
# SMTP 方式
MAIL_HOST=smtpdm.aliyun.com
MAIL_PORT=465
MAIL_ACCOUNT=noreply@notice.example.com
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();框架启动时已加载环境,不需要重复调用。

依赖坐标(版本以官方文档为准,下面不写死版本号):

xml
<!-- 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 方式发送 ​

noreply@notice.example.com 建好后即可用 SMTP 发信。要点:

  • 服务器 smtpdm.aliyun.com;
  • 端口 465 走隐式 SSL(推荐),587 走 STARTTLS,25 常被云厂商封禁;
  • 用户名是发信地址全称,密码是控制台为这个地址设置的 SMTP 密码;
  • From 用发信地址本身,不要用裸的 no-reply 之类没有域名的值。
java
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 实现里:

java
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", "noreply@notice.example.com"); // 发信地址
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());
}

三个容易踩的点:

  1. 必须设置 Body 格式(FormatType.FORM),否则还没发出请求就会抛 Content type may not be null;
  2. 参数走 Body 而不是查询串:验证码邮件的 HTML 正文可能很长,放 URL 里容易被长度限制拦截;
  3. 鉴权用 AccessKey,不是发信地址的 SMTP 密码。官方也支持用环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET 提供,代码从环境变量读取,不要硬编码。

排障时把接口返回的错误码原样带进异常信息:

java
} 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:

java
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=.." 拼成链接后发送
  }
}
java
// 启动配置里注册,否则框架取不到发信实现
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.NotFoundAccessKey 所属账号没有开通邮件推送,或 AccessKey 与邮件推送配置不在同一账号
InvalidMailAddress.NotFound发信地址不存在:控制台还没建该地址,或发信域名未验证通过
InvalidAccountName发信地址格式不对,必须是完整邮箱
Forbidden / NoPermission当前 AccessKey 没有邮件推送权限,给对应 RAM 用户授权
InvalidAccessKeyId.NotFoundAccessKey 错误或已被禁用
SignatureDoesNotMatchAccessKey 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 图片等外部资源地址都是正式域名(图片地址若指向接口域名,确认该接口可公网访问)。

官方参考:邮件推送控制台、SDK 手册、SMTP 参考、SingleSendMail 接口。

MIT Licensed