简介:这是一份阿里云邮件推送服务的官方SDK使用手册,以PDF格式发布,面向需要在业务系统中集成邮件发送、接收与管理功能的Java/PHP开发者,尤其适合初次接触阿里云邮件推送的团队或个人。压缩包内共1个PDF文件,整体大小约440KB,内容覆盖Access Key创建、Java SDK与PHP SDK的安装方式、环境要求、Maven依赖配置以及SingleSendMail等接口的调用示例,结构清晰,便于按教程段落快速查阅。已有209人学习下载,可视为入门并落地邮件推送功能的实用参考资料。读者到手后可直接对照手册完成SDK环境搭建,并基于示例代码将参数替换为自己的Access Key、发信地址和收件人,快速实现简单发信能力,减少反复查阅官方文档的成本。
1. 从阿里云邮件推送服务 SDK 手册看开发前要确认的三件事
拿到这份阿里云邮件推送服务 SDK 手册时,我通常不会从第一个章节往后读,而是先翻到初始化示例,确认三件事:AccessKey 类型、发信域名状态、接口版本。这三件事里最容易让人空转的是发信域名:代码写得再完整,只要域名没有在控制台完成 SPF 或 DKIM 验证,邮件就发不出去,或者全部进垃圾箱。另一个容易踩的是接口版本,邮件推送服务的 API 版本是 2015-11-23,而很多从其他云产品转过来的开发者会误用新版的版本号。下面只讲按这份手册从零调通、再放到生产环境这条路径,适合给业务系统做通知、验证码、账单邮件的后端开发。
2. 用 Maven 配置阿里云仓库并初始化邮件推送服务 SDK
2.1 在 settings.xml 里配置阿里云公共仓库
Java 项目第一次引入邮件推送服务 SDK 时,卡住的地方往往不是代码而是依赖下载。内网构建如果只配了中央仓库,通常会有不少依赖解析超时,尤其是 aliyun-java-sdk-core 这种带一堆 httpclient、json 传递依赖的包。常见做法是把 Maven 的 settings.xml 加一个 mirror,指向阿里云公共仓库。
<mirror> <id>aliyun-public</id> <mirrorOf>*</mirrorOf> <name>aliyun public mirror</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>这段 XML 里 mirrorOf 的写法作用域是整个构建。把 mirrorOf 写成*意味着所有仓库请求都走阿里云,适合没有私服的小团队;如果我同时要拉公司内部构件,就会改成external:*,避免把自己的私服也镜像掉。URL 用的是/repository/public,它聚合了 central 和 jcenter 的内容,DTO 类和核心包都能在上面找到。改完后不需要重新导入整个项目,只要在 IDEA 里刷新 Maven 面板,或者命令行执行mvn dependency:resolve即可。
2.2 引入依赖并区别 RPC 旧版与新版专用 Client
邮件推送服务 SDK 手册里通常存在两套用法:一套是基于 aliyun-java-sdk-core 的 RPC 风格,请求对象带着 set 方法,调client.getAcsResponse发出去;另一套是近年推广的以产品命名的新版 Client,参数更贴近 REST。老风格的好处是网上资料多,错误信息直观,适合快速跑通;新风格把 Endpoint 和签名收敛在一个 builder 里,适合新项目长期维护。我这里用老风格做示例,因为它的类名和参数在手册里最容易被搜到。
<dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>${aliyun.core.version}</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-dm</artifactId> <version>${aliyun.dm.version}</version> </dependency>版本号我习惯放在 properties 里统一定义,正式填写时以你手里的 SDK 手册标注为准,不要直接照抄网上的数字。两个依赖缺一不可:core 提供签名、HTTP 传输和 CommonRequest,dm 提供邮件推送服务相关的请求对象。如果只引入 dm 而漏掉 core,编译期通常不报错,运行期会一直报 ClassNotFoundException。
初始化客户端的代码很短:
String regionId = "cn-hangzhou"; DefaultProfile profile = DefaultProfile.getProfile( regionId, accessKeyId, accessKeySecret); IAcsClient client = new DefaultAcsClient(profile);regionId 多数情况下填cn-hangzhou,但如果控制台里看到资源属于其他地域,或者使用的是国际站资源,要改成对应地域,否则后端会提示资源不存在。AccessKey 建议用 RAM 子账号而不是主账号,子账号只需要AliyunDirectMailFullAccess权限,避免密钥泄露时整个账号被拖走。
| 参数 | 作用 | 取值建议 |
|---|---|---|
| regionId | 决定 Endpoint 与资源归属 | 与控制台一致 |
| accessKeyId | 鉴权身份 | RAM 子账号 |
| accessKeySecret | 签名密钥 | 不要写进代码仓库 |
2.3 用一次只读请求验证初始化和签名链路
初始化完成后不要急着发信,先调一个只读接口验证签名链路。这样做的原因是:发送接口一旦带着错误凭据跑起来,可能已经消耗了配额,而查询接口可以反复调。
CommonRequest request = new CommonRequest(); request.setDomain("dm.aliyuncs.com"); request.setVersion("2015-11-23"); request.setAction("DescAccountSummary"); request.setMethod(MethodType.POST); CommonResponse response = client.getCommonResponse(request); System.out.println(response.getData());如果你手里的 SDK 版本较新,编辑器可能提示 setDomain 已废弃,改成 setSysDomain 即可,作用一样。这次请求不会产生任何发送,只读取账号概览,所以适合当初始化探针。返回的 JSON 里能看到日配额、月用量之类的信息,说明签名、Endpoint、权限三条链路都通了。如果这步都过不去,后面的发送代码不用看,问题基本在凭据或网络环境。
3. 邮件推送服务 SDK 的单发、批量发送与参数取舍
3.1 先跑通单发:SingleSendMailRequest 最小代码
初始化探针通过后,第一封测试邮件用单发接口最直接。SingleSendMailRequest 是手册里最常出现的请求对象,下面的代码是我在新项目里会先跑通的最小版本。
SingleSendMailRequest request = new SingleSendMailRequest(); request.setAccountName("no-reply@example.com"); request.setAddressType(1); request.setReplyToAddress(true); request.setToAddress("user@example.com"); request.setSubject("你的登录验证码"); request.setHtmlBody("<p>验证码:123456,5 分钟内有效。</p>"); SingleSendMailResponse response = client.getAcsResponse(request); System.out.println(response.getEnvId());AccountName 必须是已经创建并通过审核的发信地址,不能临时编一个。AddressType 的 0 和 1 对应不同发件展示形式,0 表示直接用发信地址本身,1 表示用系统生成的随机地址加上你的域名;验证码场景我一般用 1,退信时可以把问题邮件隔离在随机地址上。ReplyToAddress 设置为 true 后,收件人点回复时信件回到发信地址;如果只是通知类邮件,建议设为 false,减少回信堆积和后续处理成本。
返回的 EnvId 是一次发送的流水号,不管后面有没有开回执,都应该先落库。标题和正文都有长度限制,HTML 正文里不要塞 base64 图片,常见做法是把图片放到 OSS 后传 URL。如果收件人客户端不支持 HTML,手册里还允许填 TextBody,所以事务类邮件我通常会同时提供 TextBody 和 HtmlBody。
3.2 批量发送与模板的关系
批量接口和单发不同,不是传一个收件人列表,而是先创建收件人列表和模板,再提交批量任务。手册里对应的请求会要求 TemplateName 与 ReceiversName 两个参数。
BatchSendMailRequest request = new BatchSendMailRequest(); request.setAccountName("no-reply@example.com"); request.setTemplateName("verification_code"); request.setReceiversName("order_users"); request.setAddressType(1); BatchSendMailResponse response = client.getAcsResponse(request);批量发送失败时错误不一定立即暴露在响应里,因为任务是异步的。所以调用方要保存返回的任务 ID,然后通过查询接口轮询任务状态。模板里如果要用变量,占位符需要与收件人列表文件里的字段名完全一致,少一个空格都可能导致整批失败。这里最容易出现的误用,是以为 BatchSendMail 可以像群发工具那样直接在参数里写多个收件人地址。
| 参数 | 单发 | 批量 |
|---|---|---|
| 收件人 | ToAddress | ReceiversName 引用的收件人列表 |
| 内容 | Subject + HtmlBody | 模板 TemplateName |
| 返回 | EnvId | 任务 ID |
| 适合场景 | 验证码、触发邮件 | 营销、账单、大批量通知 |
3.3 退信回执与打开追踪:TagName 和 ClickTrace 的用途
单发请求里有两个常被忽略的参数:TagName 和 ClickTrace。TagName 相当于业务标签,同一类邮件打同一个标签,在控制台和事件查询里就能按标签聚合;ClickTrace 取 0 或 1,开启后,SDK 下发的内容里会嵌入追踪链,可以统计打开和点击。
生产环境里我一般不会把这两件事当附加功能,而是把 TagName 当成业务维度的事件分区。比如周报邮件 TagName 填 weekly_report,退信回调里看到这个标签就知道是哪个场景的任务。ClickTrace 开启后会影响邮件体积和隐私,营销邮件适合开,纯事务通知建议关掉,避免收件人反感。
需要说明的是,事件结果不是同步返回的。SDK 手册里回执章节一般会讲事件通知的订阅方式,常见做法是配置到消息服务或 HTTP 端点,服务端再解析事件里的 EnvId、TagName、事件类型,把结果写回任务表。这里不要用轮询去模拟事件通知,轮询间隔短了会增加额外调用量,间隔长了又会延迟退信处理。
4. 接入业务系统时如何给邮件推送服务 SDK 设计限流和重试
4.1 控制台额度与本地限速配合
邮件推送服务在控制台上能查到的额度有两类:账号每日总量和接口调用速率。前者按自然日重置,后者按秒。SDK 本身不会帮你限速,所以业务侧要自己挡住尖峰。我一般会先压测一轮,观察返回的限流错误,再把本地速率设为控制台配额 70% 左右,留出给其他调用方的余量。
RateLimiter limiter = RateLimiter.create(20.0); // 每秒最多 20 封 ExecutorService pool = Executors.newFixedThreadPool(4); for (SendTask task : tasks) { limiter.acquire(); pool.submit(() -> processTask(task)); }RateLimiter 是 Guava 的令牌桶实现,acquire 会阻塞当前线程直到拿到许可,所以这里的 20 是全局速率。线程池 4 是为了让发信请求能并发提交,弥补每次网络往返的时间。注意这两个参数不能互相替代:只开线程池不限速会打爆配额,只限速不开线程池则发送效率太低。实际压测时先从一个较小的速率开始,比如每秒 5 封,确认没有限流错误后再逐步上调,直到接近阈值。
4.2 重试只处理网络异常,不处理业务失败
邮件发送不是幂等操作,重试必须谨慎。ClientException 里的错误码如果把额度用尽也拿来重试,结果是每重试一次就消耗一次配额,反而让限流恢复得更慢。我会区分网络类超时和控制台业务错误,网络类也只允许补偿一次,并且要保证任务状态没有在第一次调用时其实写入成功,否则用户会收到两封。
try { client.getAcsResponse(request); } catch (ClientException e) { if (e.getErrCode().contains("Timeout") || e.getErrCode().contains("Throttling")) { retrySend(request, 1); } else { recordFailure(task, e.getErrCode()); } }| 错误特征 | 是否重试 | 重试策略 |
|---|---|---|
| 连接超时、读超时 | 可重试 | 延迟 1 秒,最多 1 次 |
| 请求被限流 | 谨慎重试 | 按响应里的 Retry-After 等待 |
| 地址无效、域名未验证 | 不重试 | 记录并告警 |
| AccessKey 鉴权失败 | 不重试 | 检查凭据和权限 |
重试代码里我建议把最大次数控制在 1 到 2 次,并且每次重试前重新检查任务状态。比如第一次发送后网络超时,但后端可能已经收到了请求并成功投递,此时任务还停在 sending,如果不做状态检查就重发,用户就会收到两封验证码。生产上更保险的做法是查询接口确认没有对应 EnvId 后再补偿,虽然多了一次调用,但比重复投递好处理。
4.3 用任务表状态机抗住批量发信
批量发送不能把循环写在请求里直接跑,常见做法是先建一张 mail_task 表,每次发送请求都对应一行,状态在 pending、sending、sent、failed、bounced 之间流转。worker 从表里捞 pending 任务,捞到后立刻改成 sending。
UPDATE mail_task SET status = 'sending', worker = ?, updated_at = NOW() WHERE task_id = ? AND status = 'pending';这段 SQL 的关键是 WHERE 条件里带上 status = 'pending',这样两个 worker 同时捞同一行时,只有一个能更新成功,另一个 update 影响行数为 0,就知道任务被别的节点领走了。状态变成 sending 后,如果进程在回调返回前崩溃,任务会一直卡住,所以还要上线一个超时扫描,把超过 5 分钟还停在 sending 的任务捞出来重新置为 pending。这个状态机不需要引入消息队列,单库就够用;量大后再把任务表挪到 MQ,业务代码的发送逻辑保持不动。
5. 对照 SDK 手册排查邮件推送服务的高频错误
5.1 先看错误码还是先看 RequestId
收到异常时,我一般先看响应里的 RequestId,再看错误码。理由是文档和社区里按错误码能搜到通用原因,但 RequestId 才是阿里云侧排查的唯一凭证;如果最终要提交工单,工单里没有 RequestId,对方基本没法定位。所以在 2.3 节的探针请求里,我也建议把 RequestId 打出来存到日志。
catch (ClientException e) { log.error("send mail failed, requestId={}, errCode={}, errMsg={}", e.getRequestId(), e.getErrCode(), e.getErrMsg()); }常见的错误大致落在四个方向:域名无权使用、地址格式错误、额度超限、鉴权失败。域名无权使用对应发信地址没有完成验证或已停用,地址格式错误先看收件人是不是带了中文引号或空格,额度超限检查控制台配额,鉴权失败重点查 RAM 子账号是否授权了AliyunDirectMailFullAccess。这几个方向的修复路径完全不同,所以排查时先归类,不要对着错误信息逐字猜。
5.2 依赖版本与 Endpoint 不一致造成的诡异问题
邮件推送服务 API 的版本字段是2015-11-23。如果参考了别的云产品示例,把版本号换成了新 SDK 的日期,签名串立刻就不匹配,报错风格往往让人以为 AccessKey 有问题。另一个容易踩的是地域 Endpoint:国内版用 dm.aliyuncs.com,国际站或特定地域可能是 dm.ap-southeast-1.aliyuncs.com。手册版本页通常会列出一张 Endpoint 表,我建议把地域和 Endpoint 直接写在配置类里,不靠自动探测,减少环境差异。
# 排查 DNS 解析到的 Endpoint 是否正确 dig +short dm.aliyuncs.com这条命令是很多网络排查的起点:如果解析出来的 IP 不在预期网段,先确认是不是本地 hosts 或公司 DNS 出了问题,再回来看代码。签名和 Endpoint 混在一起报错时,先解决 DNS,再看版本号,最后才检查 AccessKey。这个顺序能省掉大量来回试错的成本。
5.3 打印 com.aliyun 日志定位签名和响应差异
<logger name="com.aliyun" level="DEBUG"/> <logger name="org.apache.http" level="DEBUG"/> <logger name="org.apache.http.wire" level="INFO"/>日志级别打开后,SDK 会打印实际请求的域名、HTTP 方法、响应状态码和响应体。重点看三个位置:签名头是否带上 date 和 Authorization、响应体里是否出现 RequestId、请求 URL 里的 Action 参数是不是预期值。生产环境不要长期开 DEBUG,因为 httpclient 的 DEBUG 会打印完整 header,其中包含授权信息;我一般在测试环境开,确认后改回 INFO。
| 日志位置 | 能看到什么 | 常见问题 |
|---|---|---|
| com.aliyun | RequestId 与错误码 | 签名或权限 |
| org.apache.http | 网络往返状态 | 超时或 TLS 报错 |
| 响应体 | 配额与错误信息 | 参数值不正确 |
6. 用 SDK 手册之外的三个手段验证一次真实发送
6.1 在 OpenAPI 调试器里先过一遍参数
本地代码一旦跟手册对不上,先别改代码,打开控制台里的 OpenAPI 调试器,选 SingleSendMail,把请求参数原样填进去。调试器返回成功后再把同样参数搬回代码。这样做的好处是把问题一分为二:调试器成功说明账号、域名、配额都正常,剩下的差异就在代码的请求对象或签名上;调试器失败则直接暴露控制台侧的问题。
6.2 从收到的邮件原文验证 SPF 和 DKIM
发送成功不等于送达,送达不等于进收件箱。测试邮件发出后,打开邮件原文,找 Authentication-Results 头。如果里面的 spf=pass 和 dkim=pass 同时出现,说明发信域名验证有效,对方的反垃圾系统会给你一个较好的初始分;如果两个都 fail,问题通常在 DNS 记录没生效,回到控制台把 DNS 记录重新验证一次。
# 保存邮件原文到 eml 文件后,检查认证结果 grep -i "authentication-results" /tmp/mail.eml这一步比看发送成功日志更接近真实链路,也是排查发送成功但收不到、或者进垃圾箱最直接的手段。注意群发测试时不要用同一个收件地址反复打,否则会被对方服务商按行为模式判定为垃圾邮件。
6.3 用 RequestId 把发送事件串成一条链路
测试时把响应里的 RequestId、EnvId 以及测试开始时间一起存进任务表。后面如果收到退信回调或打开事件,能按 EnvId 关联到具体业务任务;需要提工单时,把 RequestId 和测试时间附上,避免沟通时来回补信息。这个习惯在消息类系统里比任何日志框架都管用,因为它给每一封邮件一个从发起到回执的完整坐标。
本文还有配套的精品资源,点击获取