欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

十分钟实现发送邮件服务

程序员文章站 2022-07-02 10:29:54
发送邮件应该是网站的必备拓展功能之一,注册验证、忘记密码或者是给用户发送营销信息。 一、邮件协议 在收发邮件的过程中,需要遵守相关的协议,其中主要有: 1. 发送电子邮件的协议: ; 1. 接收电子邮件的协议: 和`IMAP`。 1.1 什么是 ? 全称为 (简单邮件传输协议),它是一组用于从源地址 ......

发送邮件应该是网站的必备拓展功能之一,注册验证、忘记密码或者是给用户发送营销信息。

一、邮件协议

在收发邮件的过程中,需要遵守相关的协议,其中主要有:

  1. 发送电子邮件的协议:smtp
  2. 接收电子邮件的协议:pop3imap

1.1 什么是smtp

smtp全称为simple mail transfer protocol(简单邮件传输协议),它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。smtp认证要求必须提供账号和密码才能登陆服务器,其设计目的在于避免用户受到垃圾邮件的侵扰。

1.2 什么是imap

imap全称为internet message access protocol(互联网邮件访问协议),imap允许从邮件服务器上获取邮件的信息、下载邮件等。imappop类似,都是一种邮件获取协议。

1.3 什么是pop3

pop3全称为post office protocol 3(邮局协议),pop3支持客户端远程管理服务器端的邮件。pop3常用于离线邮件处理,即允许客户端下载服务器邮件,然后服务器上的邮件将会被删除。目前很多pop3的邮件服务器只提供下载邮件功能,服务器本身并不删除邮件,这种属于改进版的pop3协议。

1.4 imappop3协议有什么不同呢?

两者最大的区别在于,imap允许双向通信,即在客户端的操作会反馈到服务器上,例如在客户端收取邮件、标记已读等操作,服务器会跟着同步这些操作。而对于pop协议虽然也允许客户端下载服务器邮件,但是在客户端的操作并不会同步到服务器上面的,例如在客户端收取或标记已读邮件,服务器不会同步这些操作。

二、初始化配置

2.1 开启邮件服务

本文仅以qq邮箱和163邮箱为例。

  1. qq邮箱 开启邮件服务文档
  2. 163邮箱 开启邮件服务文档

2.2 pom.xml

正常我们会用javamail相关api来写发送邮件的相关代码,但现在spring boot提供了一套更简易使用的封装。

  1. spring-boot-starter-mail:spring boot 邮件服务;
  2. 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 单元测试

  1. 测试附件邮件时,附件放在static文件夹下;
  2. 测试模版邮件时,模版放在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 发送失败情况

因为各种原因,总会有邮件发送失败的情况,比如:邮件发送过于频繁、网络异常等。在出现这种情况的时候,我们一般会考虑重新重试发送邮件,会分为以下几个步骤来实现:

  1. 接收到发送邮件请求,首先记录请求并且入库;
  2. 调用邮件发送接口发送邮件,并且将发送结果记录入库;
  3. 启动定时系统扫描时间段内,未发送成功并且重试次数小于3次的邮件,进行再次发送。

5.3 其他问题

邮件端口问题和附件大小问题。

5.4 示例代码地址

5.5 技术交流

  1. github