SpringBoot邮件发送工具类
环境:JDK8、IDEA
依赖:SpringBoot-1.5.10、spring-boot-starter-mail、spring-boot-starter-thymeleaf、spring-boot-starter-web
说明:当在本博客里面遇见不清楚的地方时,请移步其他资源补充相关知识,这里只是介绍我封装的一个邮件发送工具类而已(没有考虑性能优化,如果读者有建议可以留言,而且测试用例没有很全面,难免可能会有问题),没有很详细的邮件相关知识的介绍,望见谅。
话不多说,show the code
项目总体目录结构:
1、 pom.xml 依赖
<?xml version="1.0" encoding="UTF-8"?><project xmlns="/POM/4.0.0" xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>me.chuyf</groupId><artifactId>mail</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging><name>mail</name><description>邮件服务</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.5.10.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><!--邮件依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency><!--用于实现模板邮件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
2、邮件的基本设置
使用application.yml配置邮件的基本设置
spring:mail:host: 邮箱服务商的protocol服务器主机 #protocol: 邮件协议 #smtpdefault-encoding: UTF-8username: 指定邮箱服务商的邮箱账号 #7557*****@password: 邮箱账号密码或者三方登录授权码 #jwgteykojlf*****test-connection: truethymeleaf:cache: false #开发时关闭缓存
3、基本的服务架子
package me.chuyf.mail.service;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.core.io.FileSystemResource;import org.springframework.mail.SimpleMailMessage;import org.springframework.mail.javamail.JavaMailSender;import org.springframework.mail.javamail.MimeMessageHelper;import org.springframework.stereotype.Service;import org.thymeleaf.TemplateEngine;import org.thymeleaf.context.Context;import javax.mail.MessagingException;import javax.mail.internet.MimeMessage;import java.io.File;import java.util.Arrays;import java.util.Objects;import java.util.Set;/*** 邮件服务,实现简单文本邮件,HTML文件和附件邮件,模板邮件的发送* 支持的环境:JDK 1.8,SpringBoot 1.5.10,需要 mail-start,需要 thymeleaf 模板支持*/@Servicepublic class MailService {//默认编码public static final String DEFAULT_ENCODING = "UTF-8";//记录日志private Logger logger = LoggerFactory.getLogger(MailService.class);//本身邮件的发送者,来自邮件配置@Value("${spring.mail.username}")private String userName;//模板引擎解析对象,用于解析模板@Autowiredprivate TemplateEngine templateEngine;//邮件发送的对象,用于邮件发送@Autowiredprivate JavaMailSender mailSender;}
4、普通文本邮件的发送实现
发送普通文本邮件的大致流程如下:
1、判断是否有附件,如果有附件,那么处理的方式是不一样的(文本和二进制的区别)
2、如果是简单文本邮件,处理邮件发送的基本事物
3、如果是带附件的邮件,需要对附件做处理,同时处理邮件的基本事物
4、发送邮件
这个文本邮件发送可以实现的功能如下:
1、多收件人、多抄送人、多密送人、可带附件
2、请注意附件缺失不会导致邮件发送失败!请注意附件处理流程细节,免得出bug
不多废话,上代码
/*** 发送一个简单的文本邮件,可以附带附件:文本邮件发送的基本方法* @param subject:邮件主题,即邮件的邮件名称* @param content:邮件内容* @param toWho:需要发送的人* @param ccPeoples:需要抄送的人* @param bccPeoples:需要密送的人* @param attachments:需要附带的附件,附件请保证一定要存在,否则将会被忽略掉*/private void sendSimpleTextMailActual(String subject,String content,String[] toWho,String[] ccPeoples,String[] bccPeoples,String[] attachments){//检验参数:邮件主题、收件人、邮件内容必须不为空才能够保证基本的逻辑执行if(subject == null||toWho == null||toWho.length == 0||content == null){logger.error("邮件-> {} 无法继续执行,因为缺少基本的参数:邮件主题、收件人、邮件内容",subject);throw new RuntimeException("模板邮件无法继续发送,因为缺少必要的参数!");}logger.info("开始发送简单文本邮件:主题->{},收件人->{},抄送人->{},密送人->{},附件->{}",subject,toWho,ccPeoples,bccPeoples,attachments);//附件处理,需要处理附件时,需要使用二进制信息,使用 MimeMessage 类来进行处理if(attachments != null&&attachments.length > 0){try{//附件处理需要进行二进制传输MimeMessage mimeMessage = mailSender.createMimeMessage();MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true,DEFAULT_ENCODING);//设置邮件的基本信息:这些函数都会在后面列出来boolean continueProcess = handleBasicInfo(helper,subject,content,toWho,ccPeoples,bccPeoples,false);//如果处理基本信息出现错误if(!continueProcess){logger.error("邮件基本信息出错: 主题->{}",subject);return;}//处理附件handleAttachment(helper,subject,attachments);//发送该邮件mailSender.send(mimeMessage);logger.info("发送邮件成功: 主题->{}",subject);}catch(MessagingException e){e.printStackTrace();logger.error("发送邮件失败: 主题->{}",subject);}}else{//创建一个简单邮件信息对象SimpleMailMessage simpleMailMessage = new SimpleMailMessage();//设置邮件的基本信息handleBasicInfo(simpleMailMessage,subject,content,toWho,ccPeoples,bccPeoples);//发送邮件mailSender.send(simpleMailMessage);logger.info("发送邮件成功: 主题->{}",subject,toWho,ccPeoples,bccPeoples,attachments);}}/*** 处理二进制邮件的基本信息,比如需要带附件的文本邮件、HTML文件、图片邮件、模板邮件等等** @param mimeMessageHelper:二进制文件的包装类* @param subject:邮件主题* @param content:邮件内容* @param toWho:收件人* @param ccPeoples:抄送人* @param bccPeoples:暗送人* @param isHtml:是否是HTML文件,用于区分带附件的简单文本邮件和真正的HTML文件** @return :返回这个过程中是否出现异常,当出现异常时会取消邮件的发送*/private boolean handleBasicInfo(MimeMessageHelper mimeMessageHelper,String subject,String content,String[] toWho,String[] ccPeoples,String[] bccPeoples,boolean isHtml){try{//设置必要的邮件元素//设置发件人mimeMessageHelper.setFrom(userName);//设置邮件的主题mimeMessageHelper.setSubject(subject);//设置邮件的内容,区别是否是HTML邮件mimeMessageHelper.setText(content,isHtml);//设置邮件的收件人mimeMessageHelper.setTo(toWho);//设置非必要的邮件元素,在使用helper进行封装时,这些数据都不能够为空if(ccPeoples != null)//设置邮件的抄送人:MimeMessageHelper # Assert.notNull(cc, "Cc address array must not be null");mimeMessageHelper.setCc(ccPeoples);if(bccPeoples != null)//设置邮件的密送人:MimeMessageHelper # Assert.notNull(bcc, "Bcc address array must not be null");mimeMessageHelper.setBcc(bccPeoples);return true;}catch(MessagingException e){e.printStackTrace();logger.error("邮件基本信息出错->{}",subject);}return false;}/*** 用于填充简单文本邮件的基本信息** @param simpleMailMessage:文本邮件信息对象* @param subject:邮件主题* @param content:邮件内容* @param toWho:收件人* @param ccPeoples:抄送人* @param bccPeoples:暗送人*/private void handleBasicInfo(SimpleMailMessage simpleMailMessage,String subject,String content,String[] toWho,String[] ccPeoples,String[] bccPeoples){//设置发件人simpleMailMessage.setFrom(userName);//设置邮件的主题simpleMailMessage.setSubject(subject);//设置邮件的内容simpleMailMessage.setText(content);//设置邮件的收件人simpleMailMessage.setTo(toWho);//设置邮件的抄送人simpleMailMessage.setCc(ccPeoples);//设置邮件的密送人simpleMailMessage.setBcc(bccPeoples);}/*** 用于处理附件信息,附件需要 MimeMessage 对象** @param mimeMessageHelper:处理附件的信息对象* @param subject:邮件的主题,用于日志记录* @param attachmentFilePaths:附件文件的路径,该路径要求可以定位到本机的一个资源*/private void handleAttachment(MimeMessageHelper mimeMessageHelper,String subject,String[] attachmentFilePaths){//判断是否需要处理邮件的附件if(attachmentFilePaths != null&&attachmentFilePaths.length > 0){FileSystemResource resource;String fileName;//循环处理邮件的附件for(String attachmentFilePath : attachmentFilePaths){//获取该路径所对应的文件资源对象resource = new FileSystemResource(new File(attachmentFilePath));//判断该资源是否存在,当不存在时仅仅会打印一条警告日志,不会中断处理程序。// 也就是说在附件出现异常的情况下,邮件是可以正常发送的,所以请确定你发送的邮件附件在本机存在if(!resource.exists()){logger.warn("邮件->{} 的附件->{} 不存在!",subject,attachmentFilePath);//开启下一个资源的处理continue;}//获取资源的名称fileName = resource.getFilename();try{//添加附件mimeMessageHelper.addAttachment(fileName,resource);}catch(MessagingException e){e.printStackTrace();logger.error("邮件->{} 添加附件->{} 出现异常->{}",subject,attachmentFilePath,e.getMessage());}}}}
5、模板HTML邮件的发送实现
发现问题如下:
模板引擎解析HTML文件时,会将图片解析为可以直接访问的服务器文件路径,但是邮件是处于两个不同的网络,也就是说模板引擎解析的HTML文件里面的图片的路径是没有办法响应的,幸好邮件可以设置内联的图片资源,所以可以通过一定的方法来实现对模板HTML文件里面的图片链接进行解析,达到可以在模板邮件里面添加图片的目的,为此需要自己封装一些操作,我的一个解决方式在下面的函数里面。
不再废话,上代码
需要一个支持邮件图片内联和本地资源之间转换的支撑类,Service的内部类
/*** 用于支撑HTML内嵌图片的支持类,拥有可以传输内联图片的全部基本信息*/public final static class ImageResource {//占位符的前缀符号,用于替换字符串定位,比如:image1 在模板文件里面需要写成 #image1public static final String PLACEHOLDERPREFIX = "#";//用于文件区分,实现图片文件内联邮件发送private final String id;//这个图片需要填充到那个地方去,这个地方是一个标识,为了和其他标签区别开来,使用前缀加上标识符来进行区分,比如 :#imageOriginprivate final String placeholder;//图片的文件路径,该文件路径必须是本机文件系统的绝对路径,即可以直接 new File 的文件系统路径private final String imageFilePath;public ImageResource(String placeholder,String imageFilePath){this.placeholder = placeholder;this.imageFilePath = imageFilePath;//自动生成id,用于区分图片文件this.id = String.valueOf(System.nanoTime());}public String getId(){return id;}public String getPlaceholder(){return placeholder;}public String getImageFilePath(){return imageFilePath;}@Overridepublic String toString(){return "ImageResource{" + "id=" + id + ", placeholder='" + placeholder + '\'' + ", imageFilePath='" + imageFilePath + '\'' + '}';}
现在可以来实现模板HTML邮件的发送工具开发了
/*** 可以用来发送带有图片的HTML模板邮件** @param subject:邮件主题* @param toWho:收件人* @param ccPeoples:抄送人* @param bccPeoples:暗送人* @param attachments:附件* @param templateName:模板名称* @param context:模板解析需要的数据* @param imageResourceSet:图片资源的资源对象*/private void sendHtmlTemplateMailActual(String subject,String[] toWho,String[] ccPeoples,String[] bccPeoples,String[] attachments,String templateName,Context context,Set<ImageResource> imageResourceSet){//检验参数:邮件主题、收件人、模板名称必须不为空才能够保证基本的逻辑执行if(subject == null||toWho == null||toWho.length == 0||templateName == null){logger.error("邮件-> {} 无法继续执行,因为缺少基本的参数:邮件主题、收件人、模板名称",subject);throw new RuntimeException("模板邮件无法继续发送,因为缺少必要的参数!");}//日志这个邮件的基本信息logger.info("发送HTML模板邮件:主题->{},收件人->{},抄送人->{},密送人->{},附件->{},模板名称->{},模板解析参数->{},图片资源->{})",subject,toWho,ccPeoples,bccPeoples,attachments,templateName,context,imageResourceSet);try{//context不能够为空,需要进行检查if(context == null){context = new Context();logger.info("邮件->{} 的context为空!",subject);}//模板引擎处理模板获取到HTML字符串,这里会可能会抛出一个继承于RuntimeException的模板引擎异常String content = templateEngine.process(templateName,context);MimeMessage mimeMessage = mailSender.createMimeMessage();//默认编码为UTF-8MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true,DEFAULT_ENCODING);//处理内联的图片资源的占位转换content = handleInLineImageResourceContent(helper,subject,content,imageResourceSet);logger.info("解析邮件结果->{}",content);//处理基本信息boolean continueProcess = handleBasicInfo(helper,subject,content,toWho,ccPeoples,bccPeoples,true);if(!continueProcess){logger.error("邮件基本信息出错:主题->{}",subject);return;}//内联资源的资源附加,这个必须要放置在设置基本信息的操作后面,或者是全部内容解析完毕后才可以,不能边解析,边占位handleInLineImageResource(helper,subject,imageResourceSet);//处理附件handleAttachment(helper,subject,attachments);//发送该邮件mailSender.send(mimeMessage);logger.info("发送邮件成功:主题->{}",subject);}catch(MessagingException e){e.printStackTrace();logger.error("发送邮件失败:邮件主题->{}",subject);}}/*** 处理内嵌图片的模板HTML邮件,返回一个已经修改过后的HTML字符串** @param mimeMessageHelper:邮件信息包装类* @param subject:邮件主题* @param originContent:模板引擎所解析出来的原始HTML邮件* @param imageResourceSet:图片资源集合,用于字符集站位填充** @return :返回处理后的邮件内容*/private String handleInLineImageResourceContent(MimeMessageHelper mimeMessageHelper,String subject,String originContent,Set<ImageResource> imageResourceSet){//处理内嵌的HTML图片文件if(imageResourceSet != null&&imageResourceSet.size() > 0){//资源的占位符IDString rscId;//资源的路径String resourcePath = null;//图片的位置信息String placeHolder;//图片资源文件FileSystemResource resource;for(ImageResource imageResource : imageResourceSet){//获取图片资源的基本信息rscId = imageResource.getId();placeHolder = imageResource.getPlaceholder();resourcePath = imageResource.getImageFilePath();resource = new FileSystemResource(new File(resourcePath));//判断图片资源是否存在if(!resource.exists()){logger.warn("邮件->{} 内联图片->{} 找不到",subject,resourcePath);continue;}//替换图片资源在HTML中的位置originContent = originContent.replace("\"" + ImageResource.PLACEHOLDERPREFIX + placeHolder + "\"","\'cid:" + rscId + "\'");}}return originContent;}/*** 填充文本数据,因为数据填充必须在设置基本数据后面进行,所以讲内容和数据的填充进行分离** @param mimeMessageHelper* @param subject:邮件主题,用于日志记录* @param imageResourceSet:资源*/private void handleInLineImageResource(MimeMessageHelper mimeMessageHelper,String subject,Set<ImageResource> imageResourceSet){if(imageResourceSet != null&&imageResourceSet.size() > 0){FileSystemResource resource;for(ImageResource imageResource : imageResourceSet){resource = new FileSystemResource(new File(imageResource.getImageFilePath()));if(!resource.exists()){logger.warn("邮件->{} 的内联图片文件->{} 不存在!",subject,imageResource);continue;}try{//添加内联资源mimeMessageHelper.addInline(imageResource.getId(),resource);}catch(MessagingException e){e.printStackTrace();logger.error("邮件->{} 的内联图片文件->{} 添加错误!",subject,imageResource);}}}}/*** 传入的参数不能够为null** @param args** @return*/private boolean assertNotNull(Object... args){return Arrays.stream(args).noneMatch(Objects::isNull);}
6、接下来就是激动人心的测试环节了
注意免费邮箱设置了每天发送邮件的限制数,所以在测试的时候注意控制你的邮件发送量,有时候报错信息里面有链接时,多半是你的邮箱账号暂时被封禁了,为此我使用 4 个邮箱账号用于测试。测试前请确保你的配置是正确的。
是骡子是马,该拉出来溜溜了
测试简单文本的那个邮件发送,篇幅和机会有限,测试最复杂的一个情况
package me.chuyf.mail.service;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.mail.javamail.JavaMailSender;import org.springframework.test.context.junit4.SpringRunner;import javax.mail.MessagingException;import static org.junit.Assert.*;@RunWith(SpringRunner.class)@SpringBootTestpublic class MailServiceTest {@Autowiredprivate MailService mailService;@Autowiredprivate JavaMailSender mailSender;@Value("${spring.mail.username}")private String userName;//请把如下的信息写成你自己的邮箱信息private String mailQQ = "*****@";private String mail163 = "*****@";private String mail139 = "*****@";private String mailOutLook = "*****@";private String zipFile = "E:\\HBuilder\\plugins\\com.pandora.templates.ui_1.0.0.06081745\\templates\\project\\web.zip";private String pngFile = "F:\\sdm\\Screenshot_-09-06-22-33-58-125_com.tencent.mo.png";private String jpgFile = "F:\\sdm\\sdm.jpg";private String content = "简单的文本内容";@Test/*** 测试发送文本邮件的接口*/public void sendSimpleTextMail() throws InterruptedException{//为我的Outlook邮箱发送一封邮件,抄送我的139邮箱,密送QQ邮箱,带着三个附件mailService.sendSimpleTextMail("测试带附件、有抄送、密送的多收件人简单文本文件",content,new String[]{mailOutLook},new String[]{mail139},new String[]{mailQQ},new String[]{zipFile,jpgFile,pngFile});}}
控制台输出如下:
我的邮箱截图如下:
随便打开密送的QQ邮箱:
测试模板HTML文件的发送,先创建一个模板!
<!DOCTYPE html><html lang="en" xmlns:th=""><head><meta charset="UTF-8" /><title>邮件模板</title></head><body><h2>你好!看见请回复!</h2><p>姓名 <span th:text="${name}"></span></p><br/><p>年龄 <span th:text="${age}"></span></p><br/><p>性别 <span th:text="${sex}"></span></p><br/><img src="#sdm" width="100px" height="100px"/><br/><img src="#cyf" width="100px" height="100px"/><br/></body></html>
测试代码如下:
@Test/**** 测试模板HTML邮件*/public void sendHtmlTemplateMail() throws MessagingException{//模板解析的上下文Context context = new Context();context.setVariable("name","楚云飞");context.setVariable("age","20");context.setVariable("sex","男");//设置内联的图片资源ImageResource imageResource = new ImageResource("sdm",jpgFile);ImageResource imageResource1 = new ImageResource("cyf",pngFile);Set<ImageResource> imageResources = new HashSet<>();imageResources.add(imageResource);imageResources.add(imageResource1);mailService.sendHtmlTemplateMail("测试模板引擎的HTML解析",new String[]{mail163},new String[]{mailOutLook},new String[]{mail139},new String[]{zipFile},"mailTest",context,imageResources);}
控制台输出如下:
我的邮箱状况:
打开其中一封邮件: