邮件服务
邮件服务一般用于找回密码,邮箱绑定,面试结果通知,面试通知等。为了方便发送邮件,使用Spring Boot Mail实现邮件服务,并进一步封装了邮件验证码服务。
这里使用的是163邮箱。
前期准备
开启POP3/SMTP服务
开启163邮箱的POP3/SMTP服务,以能够发送邮件。
添加授权密码
添加依赖
添加Spring Boot Mail依赖,Redis依赖(为了缓存验证码),Thymeleaf依赖(发送HTML格式的邮件)。
<!-- 邮件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- html模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置application.properties
其中spring.mail.password是授权密码。
spring.mail.host=smtp.163.com
spring.mail.username=***@163.com
spring.mail.password=***
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
接口定义
这里定义了发送简单邮件,发送模板邮件,创建并发送邮件验证码,检查邮件验证码4个操作。其中简单邮件和模板邮件的区别在于是否使用Thymeleaf模板。
/**
* 描述:发送邮件的服务
*
* @author: xhsf
* @create: 2020/11/18 16:18
*/
public interface EmailService {
/**
* 邮箱验证码的Redis key前缀
* 推荐格式为EMAIL_AUTH_CODE_REDIS_PREFIX:{subject}:{email}
*/
String EMAIL_AUTH_CODE_REDIS_PREFIX = "email:auth-code";
@interface SendSimpleEmail{}
/**
* 发送简单邮件
*
* @errorCode InvalidParameter: 请求参数格式错误
* UnknownError: 发送邮件失败,可能是邮箱地址错误,或者网络延迟
*
* @param emailDTO 需要to、subject、text三个字段
* @param attachmentMap 附件Map,可以为null
* @return 发送结果
*/
default Result<Void> sendSimpleEmail(@NotNull EmailDTO emailDTO, Map<String, byte[]> attachmentMap) {
throw new UnsupportedOperationException();
}
@interface SendTemplateEmail{}
/**
* 发送模板邮件,使用的是velocity模板
*
* @errorCode InvalidParameter: 请求参数格式错误
* UnknownError: 发送邮件失败,可能是邮箱地址错误,或者网络延迟
*
* @param emailDTO 需要to、subject两个字段
* @param templateName 模板名,模板需要提前创建
* @param model 模板内的动态绑定的变量
* @param attachmentMap 附件Map,可以为null
* @return 发送结果
*/
default Result<Void> sendTemplateEmail(@NotNull EmailDTO emailDTO, @NotBlank String templateName,
@NotEmpty Map<String, Object> model, Map<String, byte[]> attachmentMap) {
throw new UnsupportedOperationException();
}
@interface CreateAndSendEmailAuthCode{}
/**
* 发送邮箱验证码服务
* 该服务会把邮箱验证码进行缓存
*
* @errorCode InvalidParameter: 请求参数格式错误
* UnknownError: 发送邮件验证码失败,可能是邮箱地址错误,或者网络延迟
*
* @param emailAuthCodeDTO 邮箱验证码对象
* @return Result<Void> 返回结果若Result.isSuccess()为true表示发送成功,否则发送失败
*/
default Result<Void> createAndSendEmailAuthCode(@NotNull EmailAuthCodeDTO emailAuthCodeDTO) {
throw new UnsupportedOperationException();
}
@interface CheckEmailAuthCode{}
/**
* 邮箱验证码检验验证码是否有效的服务
* 该服务检验成功后,可以清除该验证码,即一个验证码只能使用一次(EmailAuthCodeDTO.delete == true即可)
*
* @errorCode InvalidParameter: 请求参数格式错误
* InvalidParameter.AuthCode.NotFound: 找不到对应邮箱的验证码,有可能已经过期或者没有发送成功
* InvalidParameter.AuthCode.Incorrect: 邮箱验证码值不正确
*
* @param emailAuthCodeDTO 邮箱验证码对象
* @return Result<Void> 返回结果若Result.isSuccess()为true表示验证成功,否则验证失败
*/
default Result<Void> checkEmailAuthCode(@NotNull EmailAuthCodeDTO emailAuthCodeDTO) {
throw new UnsupportedOperationException();
}
}
服务实现
这里有一个关键是附件的参数是用byte[],然后再在方法里用ByteArrayResource(attachment.getValue()),这样可以方便调用方添加附件。
/**
* 描述:发送邮件的服务
*
* @author: xhsf
* @create: 2020/11/18 16:19
*/
@Service
public class EmailServiceImpl implements EmailService {
private final JavaMailSender mailSender;
private final TemplateEngine templateEngine;
private final RedisTemplate<Object, Object> redisTemplate;
@Value("${spring.mail.username}")
private String from;
public EmailServiceImpl(JavaMailSender mailSender, TemplateEngine templateEngine,
RedisTemplate<Object, Object> redisTemplate) {
this.mailSender = mailSender;
this.templateEngine = templateEngine;
this.redisTemplate = redisTemplate;
}
/**
* 发送简单邮件
*
* @errorCode InvalidParameter: 请求参数格式错误
* UnknownError: 发送邮件失败,可能是邮箱地址错误,或者网络延迟
*
* @param emailDTO 需要to、subject、text三个字段
* @param attachmentMap 附件Map,可以为null
* @return 发送结果
*/
@Override
public Result<Void> sendSimpleEmail(EmailDTO emailDTO, Map<String, byte[]> attachmentMap) {
MimeMessageHelper helper;
MimeMessage mimeMessage = mailSender.createMimeMessage();
try {
// 设置基本信息
helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom(from);
helper.setTo(emailDTO.getTo());
helper.setSubject(emailDTO.getSubject());
helper.setText(emailDTO.getText());
// 如果附件不为null,添加附件
if (attachmentMap != null) {
for (Map.Entry<String, byte[]> attachment : attachmentMap.entrySet()) {
helper.addAttachment(attachment.getKey(), new ByteArrayResource(attachment.getValue()));
}
}
} catch (MessagingException e) {
return Result.fail(ErrorCode.UNKNOWN_ERROR);
}
mailSender.send(mimeMessage);
return Result.success();
}
/**
* 发送模板邮件,使用的是velocity模板
*
* @errorCode InvalidParameter: 请求参数格式错误
* UnknownError: 发送邮件失败,可能是邮箱地址错误,或者网络延迟
*
* @param emailDTO 需要to、subject两个字段
* @param templateName 模板名,模板需要提前创建
* @param model 模板内的动态绑定的变量
* @param attachmentMap 附件Map,可以为null
* @return 发送结果
*/
@Override
public Result<Void> sendTemplateEmail(EmailDTO emailDTO, String templateName, Map<String, Object> model,
Map<String, byte[]> attachmentMap) {
MimeMessageHelper helper;
MimeMessage mimeMessage = mailSender.createMimeMessage();
try {
// 设置基本信息
helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom(from);
helper.setTo(emailDTO.getTo());
helper.setSubject(emailDTO.getSubject());
// 设置text
Context context = new Context();
context.setVariables(model);
String text = templateEngine.process(templateName, context);
helper.setText(text, true);
// 如果附件不为null,添加附件
if (attachmentMap != null) {
for (Map.Entry<String, byte[]> attachment : attachmentMap.entrySet()) {
helper.addAttachment(attachment.getKey(), new ByteArrayResource(attachment.getValue()));
}
}
} catch (MessagingException e) {
return Result.fail(ErrorCode.UNKNOWN_ERROR);
}
mailSender.send(mimeMessage);
return Result.success();
}
/**
* 发送邮箱验证码服务
* 该服务会把邮箱验证码进行缓存
*
* @errorCode InvalidParameter: 请求参数格式错误
* UnknownError: 发送邮件验证码失败,可能是邮箱地址错误,或者网络延迟
*
* @param emailAuthCodeDTO 邮箱验证码对象
* @return Result<Void> 返回结果若Result.isSuccess()为true表示发送成功,否则发送失败
*/
@Override
public Result<Void> createAndSendEmailAuthCode(EmailAuthCodeDTO emailAuthCodeDTO) {
// 发送邮件到邮箱
String authCode = AuthCodeUtils.randomAuthCode();
Map<String, Object> model = new HashMap<>();
model.put("authCode", authCode);
model.put("title", emailAuthCodeDTO.getTitle());
model.put("expiredTime", emailAuthCodeDTO.getExpiredTime());
EmailDTO emailD = new EmailDTO.Builder()
.to(emailAuthCodeDTO.getEmail())
.subject("华农招新:" + emailAuthCodeDTO.getTitle() + "验证码")
.build();
Result<Void> sendEmailAuthCodeResult = sendTemplateEmail(
emailD, "RecruitAuthCode", model, null);
if (!sendEmailAuthCodeResult.isSuccess()) {
return sendEmailAuthCodeResult;
}
// 添加邮箱验证码到缓存
String redisKey = EMAIL_AUTH_CODE_REDIS_PREFIX
+ ":" + emailAuthCodeDTO.getSubject() + ":" + emailAuthCodeDTO.getEmail();
redisTemplate.opsForValue().set(redisKey, authCode, emailAuthCodeDTO.getExpiredTime(), TimeUnit.MINUTES);
return Result.success();
}
/**
* 邮箱验证码检验验证码是否有效的服务
* 该服务检验成功后,可以清除该验证码,即一个验证码只能使用一次(EmailAuthCodeDTO.delete == true即可)
*
* @errorCode InvalidParameter: 请求参数格式错误
* InvalidParameter.AuthCode.NotFound: 找不到对应邮箱的验证码,有可能已经过期或者没有发送成功
* InvalidParameter.AuthCode.Incorrect: 邮箱验证码值不正确
*
* @param emailAuthCodeDTO 邮箱验证码对象
* @return Result<Void> 返回结果若Result.isSuccess()为true表示验证成功,否则验证失败
*/
@Override
public Result<Void> checkEmailAuthCode(@NotNull EmailAuthCodeDTO emailAuthCodeDTO) {
// 从缓存取出验证码
String redisKey = EMAIL_AUTH_CODE_REDIS_PREFIX
+ ":" + emailAuthCodeDTO.getSubject() + ":" + emailAuthCodeDTO.getEmail();
String authCode = (String) redisTemplate.opsForValue().get(redisKey);
// 验证码不存在
if (authCode == null) {
return Result.fail(ErrorCode.INVALID_PARAMETER_AUTH_CODE_NOT_FOUND, "Auth code does not exist.");
}
// 验证码不正确
if (!authCode.equals(emailAuthCodeDTO.getAuthCode())) {
return Result.fail(ErrorCode.INVALID_PARAMETER_AUTH_CODE_INCORRECT, "Auth code is incorrect.");
}
// 验证通过,如果需要删除验证码,则删除
if (emailAuthCodeDTO.getDelete()) {
redisTemplate.delete(redisKey);
}
return Result.success();
}
}
注意
如果使用模板邮件,需要在/resources/templates目录下创建html模板文件。如下示例。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<div>
<h2>华农招新:<span th:text="${title}"></span></h2>
你的验证码是:
<h1><span th:text="${authCode}"></span></h1>
<h3>该验证码的有效时间是<span th:text="${expiredTime}"></span>分钟,请尽快使用。</h3>
</div>
</body>
</html>
其他代码
EmailAuthCodeDTO
/**
* 描述:邮箱验证码,用于发送邮箱验证码时带的信息
*
* @author: xhsf
* @create: 2020/11/19 13:42
*/
public class EmailAuthCodeDTO implements Serializable {
@NotBlank
@Email
private String email;
/**
* 主题必须是该业务唯一的,不可以产生冲突,否则不准确
* 用来作为缓存时key的前缀
* 推荐格式为{服务名}:{具体业务名}
*/
@NotBlank
private String subject;
/**
* 标题,用于发送邮件验证码时标识该邮件验证码的目的
* 推荐长度不超过10个汉字
* 如“找回密码”,“邮箱绑定”等
*/
@NotBlank(groups = EmailService.CreateAndSendEmailAuthCode.class)
private String title;
/**
* 缓存键的过期时间,单位分钟
* 推荐5或10分钟
* 在调用EmailService.createAndSendEmailAuthCode()时需要带上
*/
@NotNull(groups = EmailService.CreateAndSendEmailAuthCode.class)
@Positive
@Max(10)
private Integer expiredTime;
/**
* 邮箱验证码
* 在调用EmailService.checkEmailAuthCode()时需要带上
*/
@NotBlank(groups = EmailService.CheckEmailAuthCode.class)
@AuthCode
private String authCode;
/**
* 检查成功后是否删除该键
* 在调用EmailService.checkEmailAuthCode()时需要带上
*/
@NotNull(groups = EmailService.CheckEmailAuthCode.class)
private Boolean delete;
}
EmailDTO
/**
* 描述:基本邮件传输对象
*
* @author: xhsf
* @create: 2020/11/18 16:47
*/
public class EmailDTO implements Serializable {
@NotBlank(groups = {EmailService.SendSimpleEmail.class, EmailService.SendTemplateEmail.class})
@Email
private String to;
@NotBlank(groups = {EmailService.SendSimpleEmail.class, EmailService.SendTemplateEmail.class})
private String subject;
@NotBlank(groups = EmailService.SendSimpleEmail.class)
private String text;
}