(03)Dubbo微服务实战-邮件服务

234 阅读4分钟

邮件服务

邮件服务一般用于找回密码,邮箱绑定,面试结果通知,面试通知等。为了方便发送邮件,使用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;
}