十分钟实现发送邮件服务
发送邮件应该是网站的必备拓展功能之一,注册验证、忘记密码或者是给用户发送营销信息。
一、邮件协议
在收发邮件的过程中,需要遵守相关的协议,其中主要有:
- 发送电子邮件的协议:
smtp
; - 接收电子邮件的协议:
pop3
和imap
。
1.1 什么是smtp
?
smtp
全称为simple mail transfer protocol
(简单邮件传输协议),它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。smtp
认证要求必须提供账号和密码才能登陆服务器,其设计目的在于避免用户受到垃圾邮件的侵扰。
1.2 什么是imap
?
imap
全称为internet message access protocol
(互联网邮件访问协议),imap
允许从邮件服务器上获取邮件的信息、下载邮件等。imap
与pop
类似,都是一种邮件获取协议。
1.3 什么是pop3
?
pop3
全称为post office protocol 3
(邮局协议),pop3
支持客户端远程管理服务器端的邮件。pop3
常用于离线邮件处理,即允许客户端下载服务器邮件,然后服务器上的邮件将会被删除。目前很多pop3
的邮件服务器只提供下载邮件功能,服务器本身并不删除邮件,这种属于改进版的pop3
协议。
1.4 imap
和pop3
协议有什么不同呢?
两者最大的区别在于,imap
允许双向通信,即在客户端的操作会反馈到服务器上,例如在客户端收取邮件、标记已读等操作,服务器会跟着同步这些操作。而对于pop
协议虽然也允许客户端下载服务器邮件,但是在客户端的操作并不会同步到服务器上面的,例如在客户端收取或标记已读邮件,服务器不会同步这些操作。
二、初始化配置
2.1 开启邮件服务
本文仅以
163
邮箱为例。
2.2 pom.xml
正常我们会用
javamail
相关api
来写发送邮件的相关代码,但现在spring boot
提供了一套更简易使用的封装。
-
spring-boot-starter-mail
:spring boot
邮件服务; -
spring-boot-starter-thymeleaf
:使用thymeleaf
制作邮件模版。
<!-- test 包--> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> </dependency> <!--mail --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-mail</artifactid> </dependency> <!--使用 thymeleaf 制作邮件模板 --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-thymeleaf</artifactid> </dependency> <!-- lombok --> <dependency> <groupid>org.projectlombok</groupid> <artifactid>lombok</artifactid> <scope>1.8.4</scope> </dependency>
2.3 application.yml
spring-boot-starter-mail
的配置由 mailproperties
配置类提供。
针对不同的邮箱的配置略有不同,以下是qq
邮箱和163
邮箱的配置。
server: port: 8081 #spring: # mail: # # qq 邮箱 https://service.mail.qq.com/cgi-bin/help?subtype=1&&no=1001256&&id=28 # host: smtp.qq.com # # 邮箱账号 # username: van93@qq.com # # 邮箱授权码(不是密码) # password: password # default-encoding: utf-8 # properties: # mail: # smtp: # auth: true # starttls: # enable: true # required: true spring: mail: # 163 邮箱 http://help.mail.163.com/faqdetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac2cda80145a1742516 host: smtp.163.com # 邮箱账号 username: 17098705205@163.com # 邮箱授权码(不是密码) password: password default-encoding: utf-8 properties: mail: smtp: auth: true starttls: enable: true required: true
2.4 邮件信息类
来保存发送邮件时的邮件主题、邮件内容等信息
@data public class mail { /** * 邮件id */ private string id; /** * 邮件发送人 */ private string sender; /** * 邮件接收人 (多个邮箱则用逗号","隔开) */ private string receiver; /** * 邮件主题 */ private string subject; /** * 邮件内容 */ private string text; /** * 附件/文件地址 */ private string filepath; /** * 附件/文件名称 */ private string filename; /** * 是否有附件(默认没有) */ private boolean istemplate = false; /** * 模版名称 */ private string emailtemplatename; /** * 模版内容 */ private context emailtemplatecontext; }
三、发送邮件的实现
3.1 检查输入的邮件配置
校验邮件收信人、邮件主题和邮件内容这些必填项
private void checkmail(mail mail) { if (stringutils.isempty(mail.getreceiver())) { throw new runtimeexception("邮件收信人不能为空"); } if (stringutils.isempty(mail.getsubject())) { throw new runtimeexception("邮件主题不能为空"); } if (stringutils.isempty(mail.gettext()) && null == mail.getemailtemplatecontext()) { throw new runtimeexception("邮件内容不能为空"); } }
3.2 将邮件保存到数据库
发送结束后将邮件保存到数据库,便于统计和追查邮件问题。
private mail savemail(mail mail) { // todo 发送成功/失败将邮件信息同步到数据库 return mail; }
3.3 发送邮件
- 发送纯文本邮件
public void sendsimplemail(mail mail){ checkmail(mail); simplemailmessage mailmessage = new simplemailmessage(); mailmessage.setfrom(sender); mailmessage.setto(mail.getreceiver()); mailmessage.setsubject(mail.getsubject()); mailmessage.settext(mail.gettext()); mailsender.send(mailmessage); savemail(mail); }
- 发送邮件并携带附件
public void sendattachmentsmail(mail mail) throws messagingexception { checkmail(mail); mimemessage mimemessage = mailsender.createmimemessage(); mimemessagehelper helper = new mimemessagehelper(mimemessage, true); helper.setfrom(sender); helper.setto(mail.getreceiver()); helper.setsubject(mail.getsubject()); helper.settext(mail.gettext()); file file = new file(mail.getfilepath()); helper.addattachment(file.getname(), file); mailsender.send(mimemessage); savemail(mail); }
- 发送模版邮件
public void sendtemplatemail(mail mail) throws messagingexception { checkmail(mail); // templateengine 替换掉动态参数,生产出最后的html string emailcontent = templateengine.process(mail.getemailtemplatename(), mail.getemailtemplatecontext()); mimemessage mimemessage = mailsender.createmimemessage(); mimemessagehelper helper = new mimemessagehelper(mimemessage, true); helper.setfrom(sender); helper.setto(mail.getreceiver()); helper.setsubject(mail.getsubject()); helper.settext(emailcontent, true); mailsender.send(mimemessage); savemail(mail); }
四、测试及优化
4.1 单元测试
- 测试附件邮件时,附件放在
static
文件夹下; - 测试模版邮件时,模版放在
file
文件夹下。
@runwith(springrunner.class) @springboottest public class mailservicetest { @resource mailservice mailservice; /** * 发送纯文本邮件 */ @test public void sendsimplemail() { mail mail = new mail(); // mail.setreceiver("17098705205@163.com"); mail.setreceiver("van93@qq.com"); mail.setsubject("测试简单邮件"); mail.settext("测试简单内容"); mailservice.sendsimplemail(mail); } /** * 发送邮件并携带附件 */ @test public void sendattachmentsmail() throws messagingexception { mail mail = new mail(); // mail.setreceiver("17098705205@163.com"); mail.setreceiver("van93@qq.com"); mail.setsubject("测试附件邮件"); mail.settext("附件邮件内容"); mail.setfilepath("file/dusty_blog.jpg"); mailservice.sendattachmentsmail(mail); } /** * 测试模版邮件邮件 */ @test public void sendtemplatemail() throws messagingexception { mail mail = new mail(); // mail.setreceiver("17098705205@163.com"); mail.setreceiver("van93@qq.com"); mail.setsubject("测试模版邮件邮件"); //创建模版正文 context context = new context(); // 设置模版需要更换的参数 context.setvariable("verifycode", "6666"); mail.setemailtemplatecontext(context); // 模版名称(模版位置位于templates目录下) mail.setemailtemplatename("emailtemplate"); mailservice.sendtemplatemail(mail); } }
4.2 优化
因为平时发送邮件还有抄送/密送等需求,这里,封装一个实体和工具类,便于直接调用邮件服务。
- 邮件信息类
@data public class maildomain { /** * 邮件id */ private string id; /** * 邮件发送人 */ private string sender; /** * 邮件接收人 (多个邮箱则用逗号","隔开) */ private string receiver; /** * 邮件主题 */ private string subject; /** * 邮件内容 */ private string text; /** * 抄送(多个邮箱则用逗号","隔开) */ private string cc; /** * 密送(多个邮箱则用逗号","隔开) */ private string bcc; /** * 附件/文件地址 */ private string filepath; /** * 附件/文件名称 */ private string filename; /** * 是否有附件(默认没有) */ private boolean istemplate = false; /** * 模版名称 */ private string emailtemplatename; /** * 模版内容 */ private context emailtemplatecontext; /** * 发送时间(可指定未来发送时间) */ private date sentdate; }
- 邮件工具类
@component public class emailutil { @resource private javamailsender mailsender; @resource templateengine templateengine; @value("${spring.mail.username}") private string sender; /** * 构建复杂邮件信息类 * @param mail * @throws messagingexception */ public void sendmail(maildomain mail) throws messagingexception { //true表示支持复杂类型 mimemessagehelper messagehelper = new mimemessagehelper(mailsender.createmimemessage(), true); //邮件发信人从配置项读取 mail.setsender(sender); //邮件发信人 messagehelper.setfrom(mail.getsender()); //邮件收信人 messagehelper.setto(mail.getreceiver().split(",")); //邮件主题 messagehelper.setsubject(mail.getsubject()); //邮件内容 if (mail.getistemplate()) { // templateengine 替换掉动态参数,生产出最后的html string emailcontent = templateengine.process(mail.getemailtemplatename(), mail.getemailtemplatecontext()); messagehelper.settext(emailcontent, true); }else { messagehelper.settext(mail.gettext()); } //抄送 if (!stringutils.isempty(mail.getcc())) { messagehelper.setcc(mail.getcc().split(",")); } //密送 if (!stringutils.isempty(mail.getbcc())) { messagehelper.setcc(mail.getbcc().split(",")); } //添加邮件附件 if (mail.getfilepath() != null) { file file = new file(mail.getfilepath()); messagehelper.addattachment(file.getname(), file); } //发送时间 if (stringutils.isempty(mail.getsentdate())) { messagehelper.setsentdate(mail.getsentdate()); } //正式发送邮件 mailsender.send(messagehelper.getmimemessage()); } /** * 检测邮件信息类 * @param mail */ private void checkmail(maildomain mail) { if (stringutils.isempty(mail.getreceiver())) { throw new runtimeexception("邮件收信人不能为空"); } if (stringutils.isempty(mail.getsubject())) { throw new runtimeexception("邮件主题不能为空"); } if (stringutils.isempty(mail.gettext()) && null == mail.getemailtemplatecontext()) { throw new runtimeexception("邮件内容不能为空"); } } /** * 将邮件保存到数据库 * @param mail * @return */ private maildomain savemail(maildomain mail) { // todo 发送成功/失败将邮件信息同步到数据库 return mail; } }
具体的测试详见github 示例代码,这里就不贴出来了。
五、 总结及延伸
5.1 异步发送
很多时候邮件发送并不是我们主业务必须关注的结果,比如通知类、提醒类的业务可以允许延时或者失败。这个时候可以采用异步的方式来发送邮件,加快主交易执行速度,在实际项目中可以采用mq
发送邮件相关参数,监听到消息队列之后启动发送邮件。
5.2 发送失败情况
因为各种原因,总会有邮件发送失败的情况,比如:邮件发送过于频繁、网络异常等。在出现这种情况的时候,我们一般会考虑重新重试发送邮件,会分为以下几个步骤来实现:
- 接收到发送邮件请求,首先记录请求并且入库;
- 调用邮件发送接口发送邮件,并且将发送结果记录入库;
- 启动定时系统扫描时间段内,未发送成功并且重试次数小于
3
次的邮件,进行再次发送。
5.3 其他问题
邮件端口问题和附件大小问题。
5.4 示例代码地址
-
spring boot 系列文章,欢迎关注!