抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

    最近在 Spring Boot 项目中需要添加邮件功能,在完成应用开发后对邮件服务还存在些许迷雾。因此顺便回顾下电子邮件系统的工作原理以及 SMTPMIMEPOP3IMAP 协议,做一次从基本原理到应用的知识点扫盲。

email

一、电子邮件

    邮件服务,我们常见有普通的邮件服务(即线下邮局服务)和电子邮件服务。电子邮件服务是因特网在初期就发展起来的产物,该服务能够利用网络的便捷性实现和扩展传统邮件业务,如快速发送、多用户分发和价格低廉。现代电子邮件更是具备传输包含附件、超链接、HTML格式文本图片的功能特性。

    传统邮件服务我们都知道,下面就来认识一下电子邮件服务系统及原理。(参考《计算机网络·自顶向下方法》)

1.0 电子邮件系统

emailSystem

    一个最基本的电子邮件系统如上图所示,主要包含三个部分:用户代理邮件服务器SMTP协议。用户代理为一个邮件编写发送和查看删除的客户端;邮件服务器为该系统核心,每一个邮件服务器下含开放了多个“用户邮箱”,用于存储该服务器接收到的对应的用户的邮件;SMTP协议为 TCP/IP 网络中的应用层协议,能够实现单向的邮件 Push 传输功能。

emailWorkLogic

    如上图所示(Alice 和 Bob 为系统中任意两个用户),邮件系统的工作原理为:

  1. Alice 在其代理客户端中编写好邮件,通过 SMTP 协议发送到她的邮件服务器的外出报文队列中;
  2. Alice 的邮件服务器根据 Alice 邮件中写入的接收方 Bob 的邮箱地址,将该邮件通过 SMTP 协议发送到 Bob 的邮件服务器。(例如:从谷歌的 Gmail邮箱服务器 发送到网易的 163邮箱服务器)
  3. Bob 的邮件服务器接收到邮件后,根据邮件中的写入的接收方 Bob 的邮箱地址前缀(“@”的前面部分)定位到该服务器下的 Bob 的用户邮箱,并将该邮件存入该邮箱中。
  4. Bob 可以选择直接在他的邮件服务器中查看(即如果他有该服务器的登录权限,则可以直接进入服务器查看邮件,此时不需要后面的用户代理)。也可以选择使用用户代理的方式(常见方式),在自己的用户代理中通过 POP3/IMAP/HTTP 协议从邮件服务器中 Pull 他的邮件到其代理客户端进行查看等操作。
  5. 一次邮件系统工作完成。

要点补充

  • 外出报文队列:顾名思义,为邮件服务器向其他邮件服务器发送邮件报文(即邮件内容)的队列。所有的服务器用户将邮件报文发送到队列中集中排队向外发送。如果发送失败则进行重复尝试(连接不到目的邮件服务器等问题),间隔指定时间尝试指定次数仍然失败后,邮件服务器(将会删除该邮件报文)以邮件的形式通知该用户他的邮件发送失败。
  • 邮件服务器间直连:SMTP 一般不使用中间邮件服务器发送邮件。即两个用户的邮件服务器直接进行 TCP 连接后通过 SMTP 协议进行邮件报文传输。

1.1 SMTP

    SMTP 是邮件系统的重要组成部分之一,是 Internet 电子邮件的核心。上面已经说明和体现了 SMTP 的大部分功能和特性,这里再介绍和总结一下 SMTP协议。

    SMTP协议,即 Simple Mail Transfer Protocol 简单邮件传输协议,是一个作用类似于 HTTP 协议的互联网协议标准(但比 HTTP 早)。SMTP协议 与 HTTP 使用报文的方式不同,SMTP 没有类似的传输严格格式化的报文格式,而是通过命令事务来完成协议的实现。

1.1.1 SMTP交互模型

smtpModel

    如上图SMTP模型所示,为 RFC 5321 SMTP 中的SMTP工作模型图。SMTP 实现为 C/S 架构,即含客户端和服务端。由客户端向服务端发送命令,服务端接收到命令后执行命令并反馈信息到客户端(即传统的C/S命令交互)。通过一次邮件事务的命令发送和执行完成邮件的发送。即SMTP交流模型:

    当用户需要发邮件时候,邮件发送者(Client-SMTP)建立一个与邮件接收者(Server-SMTP)通信的通道,发送者发送SMTP命令给接收者,接收者收到后对命令做回复响应。

    基本事务:通信通道被建立后,发送者发送 MAIL 命令来指定发送者的邮件,如果接受者接收这个邮件,就回复 OK;接着发送者发送 RCPT命令来指定接收者的邮箱,如果被接收同样回复OK,如果不接受则拒绝(不会终止整个通话)。接收者邮箱确定后,发送者用DATA命令指示要发送数据,并用一个.结束发送。如果数据被接收,会收到OK,然后用QUIT结束会话。

一个SMTP邮件发送例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
S: MAIL FROM:<Smith@Alpha.ARPA> # 向服务器说明邮件发送者
R: 250 OK # 服务器返回状态码和描述
S: RCPT TO:<Jones@Beta.ARPA> # 向服务器说明邮件接收者
R: 250 OK
S: RCPT TO:<Green@Beta.ARPA>
R: 550 No such user here
S: RCPT TO:<Brown@Beta.ARPA>
R: 250 OK
S: DATA # 启动邮件数据内容(ASCII编码)传输指令
R: 354 Start mail input; end with <CRLF>.<CRLF> # 服务器已经准备好接收数据并说明结束传输方式
S: Blah blah blah...
S: ...etc. etc. etc.
S: <CRLF>.<CRLF> # 发送者发送数据终止语句
R: 250 OK

进一步的简单了解请参考 SMTP协议详解,官方详细了解请参考 RFC 5321 SMTP

1.1.2 邮件报文

    SMTP协议将互联网邮件报文封装在邮件对象中。SMTP协议的邮件对象由两个部分组成:信封内容

  • 信封实际上是SMTP命令。
  • 邮件报文是邮件对象中的内容,包含首部和主体两个部分。

emailSegment

RFC 文档的对报文格式的要求:

  1. 所有报文都是由 ASCII 码组成;
  2. 报文由报文行组成,各行之间用回车(CR)、换行(LF)符分隔;
  3. 报文的长度不能超过 998 个字符;
  4. 报文行的长度 ≤78 个字符之内(不包括回车换行符);
  5. 报文中可包括多个首部字段和首部内容;
  6. 报文可包括一个主体,主体必须用一个空行与其首部分隔;
  7. 除非需要使用回车与换行符,否则报文中不使用回车与换行符。

1.1.3 SMTP 的扩展协议:MIME

    上面的邮件报文格式要求中有说到:所有的报文都是由 ASCII 码组成,那么一些非英语字符消息和二进制文件、图像、声音等非文字消息就都不能在电子邮件中传输。在互联网初期仅传输 ASCII码 还能满足需求,但到了互联网快速发展的图像视频时代就存在局限性了。因此,需要一个辅助性协议帮忙传输报文,它就是MIME

WiKi:多用途互联网邮件扩展Multipurpose Internet Mail Extensions,MIME)是一个互联网标准,它扩展了电子邮件标准,使其能够支持:

  • 非ASCII字符文本;
  • 非文本格式附件(二进位制、声音、图片等);
  • 由多部分(multiple parts)组成的消息体;
  • 包含非ASCII字元的标头资讯(Header information)。

    MIME是通过标准化电子邮件报文的头部的附加域(fields)而实现的。这些头部的附加域,描述新的报文类型的内容和组织形式。主要的附加域有三条:

1
2
3
MIME-Version: 1.0 # MIME版本
Content-Type: text/plain; charset="ISO-8859-1" # 传递的信息类型和采用的编码
Content-transfer-encoding: base64 # 编码转换方式
一个SMTP邮件发送例子

    一封MIMI邮件的源码如下(借阮大佬图片一用):

emailSourceCode

进一步了解可参考 阮一峰-MIME笔记WiKi-MIME

1.2 POP3

如上 图2-16 所示,SMTP 实现了邮件发送到邮件服务器的 Push 传输,而 POP协议 主要用于支持客户端远程下载和管理在服务器上的电子邮件。

    WiKi:邮局协议Post Office Protocol,POP)是TCP/IP协议族中的一员,主要用于支持使用客户端远程管理在服务器上的电子邮件。最新版本为POP3,全名“Post Office Protocol - Version 3”,而提供了 SSL 加密的 POP3协议 被称为 POP3S

    POP协议的远程管理是电子邮件客户端调连接邮件服务器,并下载所有未阅读的电子邮件。这种离线访问模式是一种存储转发服务,将邮件从邮件服务器端送到个人终端机器上。一旦邮件下载到个人终端上,邮件服务器上的邮件将会被删除。但目前的POP3邮件服务器大都可以“只下载邮件,服务器端并不删除”。

    POP3协议 也是通过C/S架构的命令模式完成邮件管理事务:

  1. 客户端先连接到邮件服务器,建立双方的 POP3 连接;
  2. 客户端发送命令执行对邮件服务器中的邮件管理。

常用命令参考如下:

email

1.3 IMAP

    因特网信息访问协议Internet Message Access Protocol,IMAP;以前称作交互邮件访问协议)与 POP协议一样,都是客户端对邮件服务器中邮件的管理,但 IMAP 提供了更加丰富的功能。其主要优点如下:

  • 使用IMAP4可以获得更快的响应时间。
  • 使用IMAP4可支持多个设备,同时连接到一个邮箱。
  • IMAP4支持获取部分或全部 MIME 格式的电子邮件。
  • IMAP4支持服务器查看当前的信息状态。
  • IMAP4支持在服务器访问多个邮箱。
  • IMAP4支持在服务器端搜索电子邮件。
  • IMAP4支持一个定义良好的扩展机制。

1.4 其他应用要点

    使用邮件发送,邮箱中必须开启 SMTP服务。以 QQ 为例:

email

相关服务器端口:

email

为了安全,邮件服务器都要求必须支持 SSL。QQ 邮箱收发邮件使用说明

二、Java中的邮件服务

2.1 Jakarta Mail

    Jakarta Mail(以前称为 JavaMail)是一个Jakarta EEAPI,用于通过SMTP、POP3和IMAP发送和接收电子邮件。Jakarta Mail 内置于Java EE平台中,但也提供了用于Java SE的可选包。—— WiKi 摘取。

    WiKi百科介绍及使用Demo官网详细解析和应用文档Github 中开源

Maven 坐标:

1
2
3
4
5
6
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>x.x.x</version>
<scope>compile</scope>
</dependency>

2.2 Spring Boot Mail

    Spring Boot Mail 是基于 JavaMail 封装起来的便于使用的邮件依赖,使得用户能够避免接触底层细节,更快更方便的使用邮件服务。—— Spring官方使用介绍

Maven 坐标:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

    简单来说,Spring 封装了一个JavaMailService 接口及其丰富了的具体实现 JavaMailServiceImpl类。通过JavaMailServiceImpl类send()方法能够执行发送邮件的功能。其中,简单邮件可以通过SimpleMailMessage来发送邮件,而复杂的邮件(如添加附件)可以借助MimeMessageHelper来构建MimeMessage发送邮件。

    此外,Spring Boot Mail 还支持发送HTML邮件、图片、模板邮件,具体可简单参考

简单看一眼源码

JavaMailService接口

email

JavaMailServiceImpl类

email
email

三、Spring Boot 中应用 Mail

3.1 引入和配置

引入和配置
  1. pom 引入依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
  1. application 配置文件中配置,QQ邮箱为例(参考+修改)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#  字符集编码 默认 UTF-8
spring.mail.default-encoding=UTF-8
# (必须)SMTP 服务器 host qq邮箱的为 smtp.qq.com 端口 465 587
spring.mail.host=smtp.qq.com
# (必须)SMTP 服务器端口 不同的服务商不一样
spring.mail.port=465
# SMTP 服务器使用的协议(JavaMailServiceImpl类中默认为smtp)
spring.mail.protocol=smtp
# SMTP服务器需要身份验证 所以 要配置用户密码

# (必须)发送端的用户邮箱名
spring.mail.username=business@felord.cn
# (必须)发送端的密码(授权码) 注意保密
spring.mail.password=oooooxxxxxxxx
# 指定mail会话的jndi名称 优先级较高 一般我们不使用该方式
spring.mail.jndi-name=
# 这个比较重要 针对不同的SMTP服务器 都有自己的一些特色配置该属性 提供了这些配置的 key value 封装方案 例如 Gmail SMTP 服务器超时配置 spring.mail.properties.mail.smtp.timeout= 5000
spring.mail.properties.<key> =
# 指定是否在启动时测试邮件服务器连接,默认为false
spring.mail.test-connection=false

3.2 简单使用示例

    在上述配置完成的基础上,编码如下简单使用的服务示例。引例参考

简单使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Autowired
private JavaMailSenderImpl mailSender;

public void sendMail() throws MessagingException {
//简单邮件
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setFrom("admin@qq.com");
simpleMailMessage.setTo("socks@qq.com");
simpleMailMessage.setSubject("Happy New Year");
simpleMailMessage.setText("新年快乐!");
// true 为 HTML 邮件
//messageHelper.setText(htmlStringSrc, true);
mailSender.send(simpleMailMessage);

//复杂邮件(带附件)
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage);
messageHelper.setFrom("admin@qq.com");
messageHelper.setTo("socks@qq.com");
messageHelper.setSubject("Happy New Year");
messageHelper.setText("新年快乐!");
messageHelper.addInline("doge.gif", new File("xx/xx/doge.gif"));
messageHelper.addAttachment("work.docx", new File("xx/xx/work.docx"));
mailSender.send(mimeMessage);
}

3.3 封装使用示例

    封装使用目的是为了方便业务调用,形成单独的发送邮件业务。

  1. 封装一个邮件类 **MailDO.java**
封装数据对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class MailDO {
/**
* 邮件id
*/
private String 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;
/**
* 附件
*/
private File[] files;
}
  1. 编写一个服务 **MailService.java**
邮件服务接口
1
2
3
4
5
6
7
8
9
public interface MailService {
/**
* 发送邮件
*
* @param mailDO 邮件对象
* @return 邮件对象
*/
MailDO sendMail(MailDO mailDO);
}
  1. 编写服务实现类 **MailServiceImpl.java**
接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Service
@Slf4j
public class MailServiceImpl implements MailService {

/**
* 引入 spring-boot-mail 包中的关键邮件服务实现类
*/
@Resource
private JavaMailSenderImpl mailSender;

@Override
public void sendMail(MailDO mailDO) {
try {
// 1. 检测必须的信息是否都存在
checkMail(mailDO);
// 2. 发送邮件
sendMimeMail(mailDO);
} catch (Exception e) {
log.error("发送邮件失败:", e);
mailDO.setStatus("failed");
mailDO.setError("发送邮件失败:" + e.getMessage());
}
}

/**
* 检测邮件对象必须的信息是否都存在的方法
*
* @param mailDO 邮件对象
*/
private void checkMail(MailDO mailDO) {
if (Objects.isNull(mailDO.getTo())) {
throw new RuntimeException("邮件收信人不能为空");
}
if (Objects.isNull(mailDO.getSubject())) {
throw new RuntimeException("邮件主题不能为空");
}
if (Objects.isNull(mailDO.getText())) {
throw new RuntimeException("邮件内容不能为空");
}
}

/**
* 具体邮件发送方法
*
* @param mailDO 邮件对象
*/
private void sendMimeMail(MailDO mailDO) {
try {
// 构造一个复杂邮件发送
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mailSender.createMimeMessage(), true);
mimeMessageHelper.setFrom(mailDO.getFrom());
mimeMessageHelper.setTo(mailDO.getTo().split(","));
mimeMessageHelper.setSubject(mailDO.getSubject());
mimeMessageHelper.setText(mailDO.getText());

// 抄送
if (!Objects.isNull(mailDO.getCc())) {
mimeMessageHelper.setCc(mailDO.getCc().split(","));
}
// 密送
if (!Objects.isNull(mailDO.getBcc())) {
mimeMessageHelper.setCc(mailDO.getBcc().split(","));
}
//附件
if (!Objects.isNull(mailDO.getFiles())) {
// 多个附件
for (File file : mailDO.getFiles()) {
mimeMessageHelper.addAttachment(Objects.requireNonNull(file.getName()), file);
}
}
// 发送时间
if (Objects.isNull(mailDO.getSentDate())) {
mailDO.setSentDate(new Date());
}
mimeMessageHelper.setSentDate(mailDO.getSentDate());
mailSender.send(mimeMessageHelper.getMimeMessage());
mailDO.setStatus("ok");
log.info("发送邮件成功:{} -> {}", mailDO.getFrom(), mailDO.getTo());
} catch (RuntimeException | MessagingException e) {
e.printStackTrace();
}
}
}
  1. 测试
测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// @Test
void sendMail() {
MailDO mailDO = new MailDO();

mailDO.setFrom("admin@qq.com");
mailDO.setTo("user@163.com");
mailDO.setSubject("邮件业务测试");
mailDO.setText("简单测试");

String filePath1 = "F:\\XXXProjects\\XXXService\\common\\common.iml";
String filePath2 = "F:\\XXXProjects\\XXXService\\common\\pom.xml";
File attachment1 = new File(filePath1);
File attachment2 = new File(filePath2);
File[] files = new File[]{attachment1, attachment2};

mailDO.setFiles(files);
mailService.sendMail(mailDO);
}

四、参考

  1. 个人博客-SMTP协议
  2. CSDN-SMTP协议介绍
  3. 阮一峰-MIME笔记
  4. SpringBoot 发送邮件和附件(实用版)
  5. SpringBoot 发送邮件全解析

评论

Gitalk评论系统对接至Github Issue,随心评论🐾🐾.....