使用SpringBoot集成邮件模块。

使用SpringBoot集成邮件模块。

在自己的博客新加了评论功能,为了能让自己及时的看到别人在我的个人博客留言信息,我在项目中使用了邮件模块,只要别人在我的博客进行了留言、评论,我将会在第一时间看到并且回复。【因为我也不是每时每刻都在后台系统查看信息】。所以就是方便别人也方便了自己。

在使用邮件服务的时候遇到了一些坑,在下面我也会讲我是如何解决的。
学过计算机网络后,我们都知道,发送邮件就需要使用某种协议。比如什么SMTP、IMAP、POP3.

基础知识

什么是SMTP?SMTP全称为Simple Mail Transfer Protocol(简单邮件传输协议),它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP认证要求必须提供账号和密码才能登陆服务器,其设计目的在于避免用户受到垃圾邮件的侵扰。
什么是IMAP?IMAP全称为Internet Message Access Protocol(互联网邮件访问协议),IMAP允许从邮件服务器上获取邮件的信息、下载邮件等。IMAP与POP类似,都是一种邮件获取协议。
什么是POP3?POP3全称为Post Office Protocol 3(邮局协议),POP3支持客户端远程管理服务器端的邮件。POP3常用于“离线”邮件处理,即允许客户端下载服务器邮件,然后服务器上的邮件将会被删除。目前很多POP3的邮件服务器只提供下载邮件功能,服务器本身并不删除邮件,这种属于改进版的POP3协议。

进阶知识

什么是JavaMailSender和JavaMailSenderImpl?javaMailSender和JavaMailSenderImpl是Spring官方提供的集成邮件服务的接口和实现类,以简单高效的设计著称,目前是Java后端发送邮件和集成邮件服务的主流工具。

如何通过JavaMailSenderImpl发送邮件?非常简单,直接在业务类注入JavaMailSenderImpl并调用send方法发送邮件。其中简单邮件可以通过SimpleMailMessage来发送邮件,而复杂的邮件(例如添加附件)可以借助MimeMessageHelper来构建MimeMessage发送邮件。例如:

    @Autowired
    private JavaMailSenderImpl mailSender;

    public void sendMail() throws MessagingException {
        //简单邮件
        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
        simpleMailMessage.setFrom("[email protected]");
        simpleMailMessage.setTo("[email protected]");
        simpleMailMessage.setSubject("BugBugBug");
        simpleMailMessage.setText("一杯茶,一根烟,一个Bug改一天");
        mailSender.send(simpleMailMessage);

        //复杂邮件
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage);
        messageHelper.setFrom("[email protected]");
        messageHelper.setTo("[email protected]");
        messageHelper.setSubject("BugBugBug");
        messageHelper.setText("一杯茶,一根烟,一个Bug改一天!");
        messageHelper.addInline("bug.gif", new File("xx/xx/bug.gif"));
        messageHelper.addAttachment("bug.docx", new File("xx/xx/bug.docx"));
        mailSender.send(mimeMessage);
    }

为什么JavaMailSenderImpl 能够开箱即用?所谓开箱即用其实就是基于官方内置的自动配置,翻看源码可知晓邮件自动配置类(MailSenderPropertiesConfiguration) 为上下文提供了邮件服务实例(JavaMailSenderImpl)。具体源码如下:

@Configuration
@ConditionalOnProperty(prefix = "spring.mail", name = "host")
class MailSenderPropertiesConfiguration {
    private final MailProperties properties;
    MailSenderPropertiesConfiguration(MailProperties properties) {
        this.properties = properties;
    }
    @Bean
    @ConditionalOnMissingBean
    public JavaMailSenderImpl mailSender() {
        JavaMailSenderImpl sender = new JavaMailSenderImpl();
        applyProperties(sender);
        return sender;
    }

其中MailProperties是关于邮件服务器的配置信息,具体源码如下:

@ConfigurationProperties(prefix = "spring.mail")
public class MailProperties {
    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    private String host;
    private Integer port;
    private String username;
    private String password;
    private String protocol = "smtp";
    private Charset defaultEncoding = DEFAULT_CHARSET;
    private Map<String, String> properties = new HashMap<>();
}

如何使用

一、开启邮件服务
登陆网易163邮箱,在设置中打开并勾选POP3/SMTP/IMAP服务,然后会得到一个授权码,这个邮箱和授权码将用作登陆认证。

使用SpringBoot集成邮件模块。

二、配置邮件服务

在项目中添加spring-boot-starter-mail依赖

        <!--邮件模块依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

在application.yml添加如下配置:

# tomcat port
server.port=8088

# thymeleaf

spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.check-template-location=true
spring.thymeleaf.mode=HTML
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.cache=false

# datasource
#druid数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/db_blog?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1111
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# mybatis
mybatis.mapper-locations=classpath:mapper/*.xml

#关闭默认的favicon,将自己的图标放在static下显示自己的图标
spring.mvc.favicon.enabled=false

#email
spring.mail.host=smtp.163.com
spring.mail.username= [email protected]
spring.mail.password=your_password
spring.mail.port=465
#设置为SSL协议
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
[email protected]

通过前面的进阶知识,我们知道在发送邮件前,需要先构建 SimpleMailMessage或 MimeMessage邮件信息类来填写邮件标题、邮件内容等信息,最后提交给JavaMailSenderImpl发送邮件,这样看起来没什么问题,也能实现既定目标,但在实际使用中会出现大量零散和重复的代码,还不便于保存邮件到数据库。

那么如何优雅的发送邮件呢?应该屏蔽掉这些构建信息和发送邮件的细节,不管是简单还是复杂邮件,都可以通过统一的API来发送邮件。例如: mailService.send(mailVo)

例如通过邮件信息类(MailVo) 来保存发送邮件时的邮件主题、邮件内容等信息 :

@Data
public class MailVo {
    private String id;//邮件id
    private String from;//邮件发送人
    private String to;//邮件接收人
    private String subject;//邮件主题
    private String text;//邮件内容
    private Date   sentDate;//发送时间
    private String cc; //抄送
    private String bcc; //密送
    private String status; //状态
    private String error; //报错信息
    @JsonIgnore
    private MultipartFile[] multipartFiles;//邮件附件
}

三、发送邮件和附件

  • 除了发送邮件之外,还包括检测邮件和保存邮件等操作,例如:检测邮件 checkMail(); 校验合法性:邮件收信人、邮件主题和邮件内容均为必填项,缺一不可。

  • 发送邮件 sendMimeMail(); 首先通过MimeMessageHelper来解析MailVo和构建MimeMessage,调用JavaMailSenderImpl快速发送邮件。

  • 保存邮件 sendMimeMail(); 最后将邮件保存到数据库,便于统计和追查邮件问题。

业务类 MailService 的具体源码如下:

**
 * Create bySeptember
 * 2019/4/7
 * 21:42
 */
@Service
public  class MailServiceImpl implements MailService {
    @Autowired
    private JavaMailSenderImpl mailSender;//注入邮件工具类
    @Async
    @Override
    public MailVo sendMail(MailVo mailVo) {
        try {
            checkMail(mailVo); //1.检测邮件
            sendMimeMail(mailVo); //2.发送邮件
            return saveMail(mailVo); //3.保存邮件
        } catch (Exception e) {
            mailVo.setStatus("fail");
            mailVo.setError(e.getMessage());
            return mailVo;
        }
    }

    //检测邮件信息类
    private void checkMail(MailVo mailVo) {
        if (StringUtils.isEmpty(mailVo.getTo())) {
            throw new RuntimeException("邮件收信人不能为空");
        }
        if (StringUtils.isEmpty(mailVo.getSubject())) {
            throw new RuntimeException("邮件主题不能为空");
        }
        if (StringUtils.isEmpty(mailVo.getText())) {
            throw new RuntimeException("邮件内容不能为空");
        }
    }

    //构建复杂邮件信息类
    private void sendMimeMail(MailVo mailVo) {
        try {
            MimeMessageHelper messageHelper = new MimeMessageHelper(mailSender.createMimeMessage(), true);//true表示支持复杂类型
            //mailVo.setFrom(getMailSendFrom());//邮件发信人从配置项读取
            messageHelper.setFrom(mailVo.getFrom());//邮件发信人
            messageHelper.setTo(mailVo.getTo().split(","));//邮件收信人
            messageHelper.setSubject(mailVo.getSubject());//邮件主题
            messageHelper.setText(mailVo.getText());//邮件内容
            if (!StringUtils.isEmpty(mailVo.getCc())) {//抄送
                messageHelper.setCc(mailVo.getCc().split(","));
            }
            if (!StringUtils.isEmpty(mailVo.getBcc())) {//密送
                messageHelper.setCc(mailVo.getBcc().split(","));
            }
            if (mailVo.getMultipartFiles() != null) {//添加邮件附件
                for (MultipartFile multipartFile : mailVo.getMultipartFiles()) {
                    messageHelper.addAttachment(multipartFile.getOriginalFilename(), multipartFile);
                }
            }
            if (StringUtils.isEmpty(mailVo.getSentDate())) {//发送时间
                mailVo.setSentDate(new Date());
                messageHelper.setSentDate(mailVo.getSentDate());
            }
            mailSender.send(messageHelper.getMimeMessage());//正式发送邮件
            mailVo.setStatus("ok");
        } catch (Exception e) {
            throw new RuntimeException(e);//发送失败
        }
    }

    //保存邮件
    private MailVo saveMail(MailVo mailVo) {

        //将邮件保存到数据库..

        return mailVo;
    }

    //获取邮件发信人
    public String getMailSendFrom() {
        return mailSender.getJavaMailProperties().getProperty("from");
    }

}

控制器代码如下,这里是直接获取到评论人的留的名字、邮箱、评论内容、赋值到MailVo:

    @PostMapping(value = "comment")
    @ResponseBody
    public ResponseVO comment(HttpServletRequest request, HttpServletResponse response,
                              @RequestParam Integer cid, @RequestParam Integer coid,
                              @RequestParam String author, @RequestParam String mail,
                              @RequestParam String url, @RequestParam String text) {

        if (null == cid || StringUtils.isBlank(text)) {
            return ResponseVOUtil.error(ResponseEnum.UNKNOWN_ERROR.getCode(), "请输入完整后评论");
        }

        if (StringUtils.isNotBlank(author) && author.length() > 50) {
            return ResponseVOUtil.error(ResponseEnum.UNKNOWN_ERROR.getCode(), "姓名过长");
        }

        if (StringUtils.isNotBlank(mail) && !TaleUtils.isEmail(mail)) {
            return ResponseVOUtil.error(ResponseEnum.UNKNOWN_ERROR.getCode(), "请输入正确的邮箱格式");
        }

        if (StringUtils.isNotBlank(url)&&!HttpUitl.isConnect(url)) {
            return ResponseVOUtil.error(ResponseEnum.UNKNOWN_ERROR.getCode(), "请输入正确的URL格式");
        }

        if (text.length() > 200) {
            return ResponseVOUtil.error(ResponseEnum.UNKNOWN_ERROR.getCode(), "请输入200个字符以内的评论");
        }

        author = TaleUtils.cleanXSS(author);
        text = TaleUtils.cleanXSS(text);

        author = EmojiParser.parseToAliases(author);
        text = EmojiParser.parseToAliases(text);

        Comments comments = new Comments();
        comments.setAuthor(author);
        comments.setCid(cid);
        comments.setCreated(new Date());
        comments.setIp(request.getRemoteAddr());
        comments.setUrl(url);
        comments.setContent(text);
        comments.setMail(mail);
        String result = commentService.insertComment(comments);
        if(!result.equals(WebConst.SUCCESS_RESULT)){
            return ResponseVOUtil.error(ResponseEnum.UNKNOWN_ERROR);
        }
        //发送邮件
        MailVo mailVo = new MailVo();
        mailVo.setFrom("[email protected]");
        mailVo.setTo("[email protected]");
        mailVo.setSubject("收到您朋友【"+author+"】的信件,Ta的邮箱:"+mail);
        mailVo.setText(text);
        mailService.sendMail(mailVo);
        return ResponseVOUtil.success();
    }

上面的代码,可以在本地运行,也可以部署到云服务器上运行。没有什么问题,不会出现在本地运行发布评论的时候可以调用邮件服务给你发送邮件,而部署到云服务器却接收不到邮件的情况。

为什么之前我在本地运行时候,可以接收到邮件,而发布到服务器上评论留言却没有接收到邮件呢?

阿里云服务器禁用了25端口,所以改为465端口采用SSL协议传输邮件

163网易免费邮箱相关服务器信息:
使用SpringBoot集成邮件模块。
那么将端口换了就可以了,配置如下:

#email
spring.mail.host=smtp.163.com
spring.mail.username= [email protected]
spring.mail.password=ljh18778565122
spring.mail.port=465
#设置为SSL协议
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory

效果就是:

使用SpringBoot集成邮件模块。

QQ邮箱接收到的信息如下:

使用SpringBoot集成邮件模块。