1 关于邮箱的前置知识
1.1 概念部分
邮箱服务器和协议是电子邮件系统中不可或缺的两个组成部分,它们共同确保了电子邮件的顺利发送、接收和管理。
1.1.1 服务器和协议
邮箱服务器
比如常见的QQ邮箱,网易邮箱。
邮箱服务器是一种负责电子邮件收发管理的计算机系统。它通过互联网连接到客户端设备,允许用户通过电子邮件与其他用户进行交流和沟通。邮箱服务器不仅负责接收、存储和转发电子邮件,还提供了多种管理和安全功能,如身份认证、邮箱管理、邮件过滤等。
邮箱协议
邮箱协议是指用于电子邮件收发过程中,客户端与服务器之间通信所遵循的规则和标准。常见的邮箱协议包括SMTP
、POP3
和IMAP
。
qq
邮箱中的邮箱协议
SMTP(Simple Mail Transfer Protocol
,简单邮件传输协议):
- 功能:
SMTP
是一种用于发送电子邮件的协议。它定义了一组规则,使得发送方能够将邮件传送到指定的接收方服务器。 - 特点:
SMTP
协议简单、高效,适用于大规模邮件的传输。然而,它并不支持邮件的撤回、加密传输等功能。
POP3(Post Office Protocol 3
,邮局协议第三版):
- 功能:
POP3
是一种用于接收电子邮件的协议。它允许用户从邮件服务器上下载邮件到本地计算机进行查看和管理。 - 特点:
POP3
协议简单易用,适用于个人用户和小型企业。但是,由于它不支持邮件在服务器上的同步更新,因此不适合多设备同时使用的场景。默认情况下,当邮件被下载到本地后,服务器上的邮件会被标记为已删除,以节省服务器存储空间。
IMAP(Internet Message Access Protocol
,互联网邮件访问协议):
- 功能:
IMAP
是一种更高级的电子邮件接收协议。与POP3
不同,IMAP允许用户在服务器上直接管理邮件,实现多设备间的邮件同步。 - 特点:
IMAP
协议支持多设备同步、实时更新等功能,非常适合多设备用户和企业使用。然而,由于其工作原理相对复杂,因此可能会消耗更多的网络资源和服务器资源。
常见SMTP
协议邮箱服务器
邮箱服务提供商 | SMTP服务器域名 | 备注 |
---|---|---|
Gmail(谷歌邮箱) | smtp.gmail.com | 通常使用端口465(SSL)或587(TLS) |
Outlook/Hotmail(微软邮箱) | smtp.outlook.com | 原为smtp.live.com,通常使用端口587(TLS) |
QQ邮箱 | smtp.qq.com | 通常使用端口465(SSL)或587(TLS),需要授权码进行认证 |
网易邮箱(163邮箱) | smtp.163.com | 通常使用端口25、465(SSL)或994(IMAP SSL),需要授权码进行认证 |
网易邮箱(126邮箱) | smtp.126.com | 类似163邮箱的配置 |
新浪邮箱 | smtp.sina.com.cn | 通常使用端口25或465(SSL) |
搜狐邮箱 | smtp.sohu.com | 通常使用端口25或465(SSL) |
阿里云邮箱 | smtp.aliyun.com | 根据实际域名配置,通常使用SSL加密 |
1.1.2 邮件
一封完整的邮件包括:邮件头和邮件体以及附件。
邮件头
- 发件人(
From
):标识邮件的发送者,包括邮箱地址和可选的姓名。 - 收件人(
To
):邮件的直接接收者,可以是单个或多个邮箱地址。 - 抄送人(
CC
):邮件的抄送对象,将收到邮件的副本,但不是主要行动对象。 - 密送人(
BCC
):邮件的密送对象,其邮箱地址不会显示在邮件的收件人列表中,但会收到邮件副本。 - 主题(
Subject
):邮件的标题,用于概括邮件的主要内容或目的。 - 其他可选头部字段:如日期(Date)、回复地址(Reply-To)、邮件ID(Message-ID)等,这些字段提供了邮件的额外信息。
邮件正文(Body
)
- 内容类型:可以是纯文本
(text/plain)
或HTML
格式(text/html)
,HTML
格式允许使用富文本和格式化元素。 - 内容:邮件的具体内容,可以包含文字、链接、图片等。对于HTML格式的邮件,内容将以HTML标签的形式呈现。
附件(Attachments
)
- 邮件可以包含一个或多个附件,如文档、图片、音频、视频文件等。
1.2 授权
邮件中的授权主要涉及邮箱服务提供商为增强账户安全性而推出的一种验证机制,特别是针对第三方客户端的登录。以下是关于邮件中授权的几个关键点:
1.2.1 授权码的概念
授权码是邮箱服务提供商为登录第三方客户端(如邮件客户端软件、手机APP等)而推出的专用密码。它类似于邮箱密码,但具有更高的安全性和灵活性。授权码会出现失效、过期等情况,用户可以随时关闭服务使授权码失效,从而增强账户的安全性。
1.2.2 授权码的作用
- 增强安全性:通过授权码登录第三方客户端,即使邮箱密码泄露,也不会直接影响到这些客户端的安全性。
- 灵活性:用户可以根据需要为不同的客户端设置不同的授权码,或者随时关闭不再使用的授权码。
- 兼容性:授权码适用于多种邮件协议,如POP3、IMAP、SMTP等,方便用户在不同客户端上收发邮件。
1.2.3 如何获取授权码
以QQ邮箱为例,获取授权码的步骤通常如下:
- 登录邮箱:首先,用户需要登录网页版的邮箱账户。
- 进入设置:在邮箱首页或设置菜单中,找到并进入账户设置或安全设置等选项。
- 开启服务:在账户设置或安全设置中,找到POP3/SMTP/IMAP等服务选项,并开启这些服务。注意,开启这些服务时可能需要进行身份验证,如发送短信验证码等。
- 获取授权码:开启服务后,按照页面提示发送短信验证码或进行其他身份验证。验证成功后,系统将生成一个授权码,并显示在页面上。用户需要复制并保存这个授权码,以便在第三方客户端中使用。
1.2.4 注意事项
- 授权码的唯一性:虽然授权码可以重复使用,但建议用户为每个客户端设置不同的授权码,以提高安全性。
- 授权码的时效性:授权码可能会因为用户更改密码、关闭服务或邮箱服务提供商的策略调整而失效。因此,用户需要定期检查和更新授权码。
- 安全性保护:用户应妥善保管授权码,避免泄露给他人。同时,建议定期更换授权码,以提高账户的安全性。
授权码用在Java
程序中替代我们在网页上登录诸如163
邮箱网址的密码,用户名还是网页的用户名!
1.3 邮件收发基本流程
网络图片
1.3 使用协议收发邮件
此章节内容参考:邮件基本概念及发送方式
协议标准参考:RFC
1.3.1 SMTP
协议发送邮件
SMTP
命令
1)telnet
连接邮件服务器
# telnet是一种网络协议,主要用于远程登录到另一台计算机或网络设备(如服务器、路由器等)并执行命令。
telnet smtp.163.com 25
2)helo user_name
SMTP
协议握手
# 输入 ehlo clcao(任意) 并回车,向服务器打招呼,或者命令 helo
ehlo clcao
通过ehlo
命令可以看到服务器支持的认证方式
3)认证登录
# 身份验证机制(mechanisms):login,其他还有plain、DIGEST-MD5、NTLM、OAuth 2.0等,具体还需要看邮箱服务器支持哪些认证机制
auth login
# 紧接着输入 base64 编码后的用户名
Y2FvY2FpbGlhbmdfc2dwYUAxNjMu29t
# 紧接着输入 base64 编码后的授权码
T0ZQVVhXUVlQU1QSkZSQw==
# 认证成功后的回显
235 Authentication successful
4)编写收发件人邮箱地址
# 发件人
mail from:<xxx@163.com>
# 收件人(可以是任意邮箱,包括发件人自己)
rcpt to:<xxx@163.com>
5)编写邮件内容(邮件头,邮件体)
# data 后直接回车会提示:
data
# 邮件头:发件人
from:<xxx@163.com>
# 邮件头:收件人
to:<xxx@163.com>
# 邮件头:主题
subject:Test For SMTP
# 邮件头和邮件体需要一行空格
# 邮件体:正文
Hello SMTP!!!
# . 后面再跟一个回车换行结束,即 <CRLF>.<CRLF> 为结束标记
.
这一步邮件其实已经编写好发送出去了!
6)断开服务器连接
quit
1.3.2 POP3
协议接收邮件
POP3
命令
1)telnet
连接邮箱服务器
telnet pop.163.com 110
2)登录
# 输入用户名,非 Base64 编码
user xxx.163.com
# 输入授权码,非 Base64 编码
OFPUXWQYABBPJFRC
登录后的命令
1)查询概览信息
# <CRLF>代表回车换行
stat <CRLF>
2)查询邮件唯一标识符
# uidl 2 表示查看序号为2的邮件唯一标识
uidl <id>
3)列出邮件信息
# id可选参数,为邮件需号
list <id>
4)获取邮件
retr <id>
5)删除邮件
dele <id>
6)清除删除标记
rset <CRLF>
2 Java
收发邮件技术
基于上面的知识,使用Java
程序发送邮件,最少需要知道:
- 使用哪个邮箱服务器,比如
smtp.qq.com
。 - 发件人的用户名和授权码(等同登录网站用的密码)
- 邮件
- 收件人(至少得知道发送给谁)
- 内容(不能为空)
关于使用1.3 使用协议收发邮件章节,在断点调试
JakartaMail
源码时候可以更容易理解里边的一些概念,比如认证器LoginAuthenticator
,对应auth login
这行命令。
Java收发邮件主要框架就是JakartaMail
以及对其封装后的spring-boot-starter-mail
,但SpringBoot
本质技术还是JakartaMail
。
关于JavaxMail
和JakartaMail
Javax.*
是java标准的一部分,但是没有包含在标准库中,一般属于标准库的扩展。通常属于某个特定领域,不是一般性的api。
Jakarta
为雅加达城市,位于爪哇岛,猜测命名同源Java
。为JavaEE
的后续版本,包括Servlet
之前也是javax包下的
,现在都迁移到Jakata
包下了,所以Java Mail
在依赖包这一块也存在区分的,过早版本都是javax.mail
现在都是jakarta.mail
(推荐)。
1)javax.mail
包
<!--JavaMail基本包-->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</dependency>
<!--邮件发送的扩展包-->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</dependency>
2)com.sun
下的mail
包
<!--使用Sun提供的Email工具包-->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
</dependency>
3)Jakarta.mail
(推荐)
<!--jakarta.mail间接依赖了jakarta.activation-->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
4)Jakarta.mail-api
可以选择不同的实现,这里是angus
实现
<!--jakarta.mail-api只是接口包,并不包含实现(无法单独使用)-->
<dependency>
<groupId>jakarta.mail</groupId>
<artifactId>jakarta.mail-api</artifactId>
<version>2.1.2</version>
</dependency>
<!--jakarta.mail-api的实现-->
<dependency>
<groupId>org.eclipse.angus</groupId>
<artifactId>angus-activation</artifactId>
<version>2.0.2</version>
</dependency>
2.1 JakartaMail
官网手册:Jakarta Mail 2.1
1.2 快速入门
添加依赖
<!--jakarta.mail-->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
发送邮件
public static void sendMail() {
// 1.封装属性
Properties props = System.getProperties();
props.setProperty("mail.host",emailHost);
props.setProperty("mail.smtp.auth","true");
log.debug(System.getProperty("user.name"));
// 2. 创建 Session 对象
Session session = Session.getDefaultInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(fromUser, autoCode_163);
}
});
session.setDebug(true);
try {
// 3. 构建邮件消息对象
MimeMessage msg = new MimeMessage(session);
msg.setFrom(user + "<" + fromUser +">");
msg.setSubject("Test For JakartaMail");
msg.setText("Hello JakartaMail!");
msg.setRecipient(Message.RecipientType.TO,new InternetAddress(toEmail)); // 收件人
msg.setRecipient(Message.RecipientType.CC,new InternetAddress(toEmail)); // 抄送人
msg.setRecipient(Message.RecipientType.BCC,new InternetAddress(toEmail)); // 密送人
// 4. 发送邮件消息
Transport.send(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
接收邮件
@Test
public void testReceive() throws MessagingException, IOException {
String host = "pop3.163.com";
String protocol = "pop3";
// 1. 封装属性
Properties props = new Properties();
props.setProperty("mail.host",host);
props.setProperty("mail.store.protocol",protocol);
// 2. 获取连接
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user, password);
}
});
session.setDebug(true);
// 3. 获取 Store 对象
Store store = session.getStore();
store.connect();
// 打开邮件夹
Folder folder = store.getFolder("INBOX");
folder.open(Folder.READ_WRITE);
// 4. 获取所有邮件
Message[] messages = folder.getMessages();
System.out.println("邮件数量:" + messages.length);
// 5. 查看邮件
MimeMessage msg = (MimeMessage) messages[0];
Address[] from = msg.getFrom();
Address[] tos = msg.getAllRecipients();
String subject = msg.getSubject();
Object content = msg.getContent();
System.out.println("发件人:" + Arrays.toString(from));
System.out.println("收件人:" + Arrays.toString(tos));
System.out.println("主题:" + subject);
System.out.println();
System.out.println("正文:" + content);
}
收发邮件流程图
1.3 邮件发送原理
发送邮件的本质其实是通过Socket
建立与服务器的连接,Socket
连接就可以获取输入输出流,根据输出流可以写命令,即SMTP
标准协议规定的命令,而输入流则可以读取服务器的响应内容。
所以最核心的本质还是1.3.1 SMTP
协议发送邮件
自定义发送邮件(不是用JakartaMail
框架)
基本思路:
- 创建
Socket
建立与服务器(smtp.163.com 25
)的连接 - 初始化流信息
InputSteam
读取响应,OutputStream
写命令 - 发送命令
HELO
问候AUTH LOGIN
身份认证- 用户名和密码的
Base64
输入 MAIL FROM
设置发件人RECP TO
设置收件人DATA
开始编写邮件内容from:
发件人to:
收件人subject:
主题- 正文
<CRLF>.<CRLF>
结束DATA
邮件编写
- 关闭资源
@Test
public void testMyMail() throws IOException, InterruptedException {
String host = "smtp.163.com";
int port = 25;
// 1. 与邮件服务器建立连接
System.out.println("1. 与邮件服务器建立连接");
Socket socket = new Socket(host, port);
initStream(socket);
// 2. 发送问候
System.out.println("2. 发送问候");
String helo_cmd = "HELO " + host;
sendCmd(helo_cmd.getBytes(StandardCharsets.UTF_8));
if (220 != readResponse()) {
throw new RuntimeException("HELO 命令执行失败!");
}
// 3. 授权命令
System.out.println("3. 授权命令");
String auth_cmd = "auth login";
sendCmd(auth_cmd.getBytes(StandardCharsets.UTF_8));
if (readResponse() != 250) {
throw new RuntimeException("auth login 命令执行失败!");
}
// 4. 验证用户名和密码
System.out.println("4. 验证用户名和密码");
sendCmd(Base64.getEncoder().encode(user.getBytes(StandardCharsets.UTF_8)));
System.out.println(Base64.getEncoder().encodeToString(user.getBytes(StandardCharsets.UTF_8)));
if (readResponse() != 334) {
throw new RuntimeException("用户名输入失败");
}
sendCmd(Base64.getEncoder().encode(password.getBytes(StandardCharsets.UTF_8)));
System.out.println(Base64.getEncoder().encodeToString(password.getBytes(StandardCharsets.UTF_8)));
if (readResponse() != 334) {
throw new RuntimeException("密码输入失败");
}
if (readResponse() != 235) {
throw new RuntimeException("身份认证失败");
}
// 5. 发送邮件
System.out.println("5. 发送邮件");
String mailFrom_cmd = "mail from:<" + user + ">";
sendCmd(mailFrom_cmd.getBytes(StandardCharsets.UTF_8));
if (readResponse() != 250) {
throw new RuntimeException("设置from失败");
}
String rcptTo_cmd = "rcpt to:<" +user +">";
sendCmd(rcptTo_cmd.getBytes(StandardCharsets.UTF_8));
if (readResponse() != 250) {
throw new RuntimeException("设置to失败");
}
// 6. 编写邮件
System.out.println("6. 编写邮件");
sendCmd( "data".getBytes(StandardCharsets.UTF_8) );
if (readResponse() != 354) {
throw new RuntimeException("data命令失败");
}
// 编写邮件内容:邮件头from、to、subject,邮件体 txt StringBuffer msg_cmd = new StringBuffer();
String from = "from:<" + user + ">";
String to = "to:<" + user + ">";
String subject = "subject:Test For MyMail!";
String txt = "Hello MyMail";
msg_cmd.append(from).append(CRLF)
.append(to).append(CRLF)
.append(subject).append(CRLF)
.append(CRLF)
.append(txt)
.append(CRLF).append(".");
System.out.println(msg_cmd);
sendCmd(msg_cmd.toString().getBytes(StandardCharsets.UTF_8));
if (readResponse() != 250) {
throw new RuntimeException("邮件发送失败!");
}
// 关闭资源
serverOutput.close();
serverInput.close();
socket.close();
}
private void initStream(Socket socket) throws IOException {
log.debug("建立连接,isConnected ? {}",socket.isConnected());
log.debug("初始化流对象...");
serverOutput = new BufferedOutputStream(socket.getOutputStream());
serverInput = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
private synchronized int readResponse() throws IOException {
log.debug("读取响应开始...");
String line;
StringBuilder sb = new StringBuilder();
do {
line = serverInput.readLine();
sb.append(line);
sb.append("\n");
} while (isNotLastLine(line));
log.debug("响应内容:{}",sb);
return Integer.parseInt(sb.substring(0,3));
}
private boolean isNotLastLine(String line) {
return line != null && line.length() >= 4 && line.charAt(3) == '-';
}
private synchronized void sendCmd(byte[] cmdBytes) throws IOException {
log.debug("发送命令:{}","`" + new String(cmdBytes) + "`");
serverOutput.write(cmdBytes);
serverOutput.write(CRLF.getBytes(StandardCharsets.UTF_8));
serverOutput.flush();
log.debug("命令发送完毕.");
}
1.4 深入理解JakartaMail
1.4.1 JakartaMail
组成
Session
:定义了发邮件的客户端和接收邮件的网络会话。Message
:定义邮件信息的一系列属性和内容,包括收件人地址和邮件主题等。Transport
:真正与服务器建立连接的对象,获取Socket
连接并且通过输入输出流读写响应和发送命令Store
:负责与服务器建立连接接收消息存储在Folder
邮件夹对象中
Session
类
用于构建与邮件服务器之间的会话,包括信息有:服务器host、认证器(Authenticator
)这里存在用户名和密码信息、Transport
对象(该对象才真正负责连接服务器)
方法 | 描述 |
---|---|
Transport getTransport() | 获取mail.transport.protocol 属性指定的transport (实际上不指定默认也是smtp ) |
Transport getTransport(String protocol) | 获取指定协议的transport |
Transport getTransport(Provider provider, URLName url) | 获取指定provider 和urlname 的transport |
Transport getTransport(URLName url) | 指定URLName 构建Transport 对象 |
Transport getTransport(Provider provider) | 指定provider 构建Transport 对象 |
Transport getTransport(Address address) | 指定地址构建Transport 对象 |
无论何种方法,最后都会走Transport getTransport(Provider provider, URLName url)
这里涉及两个信息:具体实现Provider
,URLName
。其中URLName
封装的信息有:
除了指定provider
和urlName
的重载方法,其余方法都是根据协议构建Transport
,因此只要没有具体指明provider
和urlName
,都遵循以下规则获取协议:
- _默认存在配置文件
/META-INF/javamail.default.address.map
定义在mail.jar
包中默认存在一个key-value
为:rfc822:smtp
。 - _在类路径下
META-INF/javamail.address.map
添加配置文件指定协议。 - _
${java.home}/conf/javamail.address.map
下的配置 - _如果都没有则会添加一个默认的协议
rfc822:smtp
。
如果拿快速入门来看,其实最终都是默认SMTP
协议构建Transport
对象,最终也就是:
- 默认的
Provider
实现(SMTPTransport
类) - 默认的
URLName
(new URLName("smtp", null, -1, null, null, null)
)
虽然默认的URLName
只有协议信息,但是在SMTPTransport
构造器实例化对象时候初始化做了很多事情!
SMTPTransport
构造器执行
- 根据属性
mail.smtp.ssl.enable
决定默认端口,如果为true
则为465
,如果为false
则为25
- 设置一堆属性,具体参考下表
- 创建默认的认证器,用于身份认证机制
LoginAuthenticator
PlainAuthenticator
DigestMD5Authenticator
NtlmAuthenticator
OAuth2Authenticator
由于SMTPTransport extends Transport extends Service
,所以实例化SMTPTransport
之前会执行Servie
的构造。
Servcie
构造器执行
- 根据属性
mail.smtp.host
获取host
信息- 如果没有则根据属性
mail.host
获取
- 如果没有则根据属性
- 根据属性
mail.smtp.user
获取user
信息- 如果没有则根据属性
mail.user
获取
- 如果没有则根据属性
- 构建
URLName
这也就是为什么在构建SMTPTransport
对象时并不关注URLName
对象创建的原因,因此在父类中已经继承了关键信息:host,user
总结:创建Session
对象主要目的是为了封装属性(host,user,password
),即便没有手动创建Session
,也可以通过System.getProperties()设置属性创建默认的Session
Session并不直接连接服务器,而是通过Transport
类实现,而Transport
的构建依赖于继承Service
该类在初始化时候,会根据属性读取host,user
信息
Transport
类
发送邮件的静态方法
方法 | 描述 | 发送之前saveChanges() |
---|---|---|
static void send(Message msg) | 为msg 定义的每个收件人发送邮件 | true |
static void send(Message msg, Address[] addresses) | 忽视msg 指定的收件人,向指定的地址发送邮件 | true |
static void send(Message msg, String user, String password) | 为msg 定义的每个收件人发送邮件,使用指定的用户名和密码进行身份验证 | true |
static void send(Message msg, Address[] addresses,String user, String password) | 忽视msg 指定的收件人,向指定的地址发送邮件,使用指定的用户名和密码进行身份验证 | true |
所有静态方法最后都会走到下面👇的实例方法发送消息!
实例发送消息方法
方法 | 描述 |
---|---|
void sendMessage(Message message, Address[] addresses) | 向指定的地址发送消息 |
具体规则为:
- _无论使用哪个静态重载方法,最后都走实例方法
static void send(Message msg, Address[] addresses, String user, String password
- _如果
msg
存在session
则获取msg
中定义的session
创建Transport
对象 - _如果不存在则,创建默认的
Transport
对象(Session.getDefaultInstance(System.getProperties(), null)
)
所以,此处代码可以变通
// 由于创建默认的 Transport 对象属性为系统属性,因此必须设置host,user,pass,相关信息在系统属性
Properties props = System.getProperties();
props.setProperty("mail.host",emailHost);
props.setProperty("mail.user",user);
props.setProperty("mail.smtp.auth","true");
// 构建邮件消息对象 这里省略了第一步创建 Session 对象
MimeMessage msg = new MimeMessage((Session) null);
connect()
重载方法
方法 | 描述 |
---|---|
void connect() | 通用的获取连接方法,如果连接成功,发送ConnectionEvent 事件 |
void connect(String host, String user, String password) | 通过简单的用户名和密码授权获取指定服务器的连接 |
void connect(String user, String password) | 通过简单的用户名和密码授权获取当前服务器的连接 |
void connect(String host, int port, String user, String password) | 指定端口,功能同上(上面三个方法最后都是调用此方法) |
发送消息之前,都会通过transport.connect()
进行连接!而最终方法都是:void connect(String host, int port, String user, String password)
所以连接的核心信息是:host,port,user,password
具体获取规则如下:
- 如果
Transport.send()
或者transport.sendMessage()
没有指定host,user,password
,则从URLName
获取 - 如果为空则从属性
mail.smtp.host|user
获取。(有点无语,因为Service
只要加载就会读取这些属性封装到URLName
上) - 如果还为空则从属性
mail.host|user
获取 - 如果
user
还为空,直接读取系统属性System.getProperty("user.name")
- 如果密码为空,则从
session.getPasswordAuthentication
获取
// 如果没有手动构建URLName 对象,那么密码将从这里获取
session.setPasswordAuthentication(transport.getURLName(),new PasswordAuthentication(fromUser,authCode));
无论是否获取到这些信息,最终都将执行连接的真正方法:protocolConnect(host, port, user, password)
protocolConnect
执行
- 获取属性
mail.smtp.auth
,判读是否需要身份认证,默认为false
- 获取属性
mail.smtp.ehlo
,如果为false
将默认执行helo
问候,默认为true
(这个会导致服务器响应内容为空,导致身份认证失败,不推荐!) - 构建
Socket
连接并执行ehlo
或者helo
问候 - 如果设置了属性
mail.smtp.auth
为true
,或者user
和password
都不为空则进行身份认证- 发送命令
AUTH LOGIN
- 发送用户名和密码
- 发送命令
也可以直接构建socket
连接
SMTPTransport transport = ((SMTPTransport) session.getTransport());
transport.connect(new Socket(emailHost,25));
到这里相当于完成了命令发送邮件的两个步骤:telnet smtp.163.com
和 helo hello
以及auth login
!。
连接完成后,才是发送消息,因此,如果自己调用实例方法sendMessage()
,一定需要先连接。
transport.connect();
transport.sendMessage(msg,msg.getAllRecipients());
sendMessage()
执行
mailFrom()
设置发件人rcpt()
设置收件人- 写消息
Message
类
参考章节The Message Class
Message
类定义了消息的属性和内容,属性包括收发件人的地址信息,同时也定义了内容结构,比如HTML
格式,包括Content-type
,其中内容由DataHandler
类表示。
一个消息对象应该包括:
- 收发件人地址信息
- 消息头属性(
Content-Type
) - 消息体内容
- 消息状态(是否已读)
MimeMessage
1.4.2 常用API
属性设置相关
// 1. 主题Subject获取和设置
public String getSubject() throws MessagingException;
public void setSubject(String subject) throws MessagingException; public
// 2. Header获取和设置
String[] getHeader(String name) throws MessagingException;
public void setHeader(String name, String value) throws MessagingException;
// 3. 内容Content获取和设置
public Object getContent() throws MessagingException;
public void setContent(Object content, String type) throws MessagingException
Header
头
保存邮件到邮件夹Folder
# 保存邮件
public void saveChanges() throws MessagingException;
为消息对象产生字节流
public void writeTo(OutputStream os) throws IOException, MessagingException;
发送消息时候就是写数据,使用message.writeTo()
。
Part
接口
Message
和BodyPart
都实现了此接口,通用的Part
抽象,定义了一系列Header
属性,并提供了getter,setter
方法。
消息属性
即定义在Message
类上扩展的属性,包括:
- 发件人
from
- 收件人
recipitents
- 发件时间
received date
- 主题
subject
- 标记
falg
Content-Type
属性
contentType
属性按照MIME
类型规范(RFC 2045)指定内容的数据类型。MIME类型由声明内容一般类型的主类型和指定内容特定格式的子类型组成。MIM
E类型还包括一组可选的特定类型参数。
Address
类
封装电子邮件地址的类。
BodyPart
类
消息体的部分,一个Multipart
可以包含多个BodyPart
。Multipart
作为BodyPart
的容器
MultiPart
要求Content-Type
是multipart
类型,作为BodyPart
的容器。
Message
与MultiPart
常见的MultiPart
类型
Flag
类
消息的状态信息,包括邮件是否已读,是否删除,是否答复等。
Flag类型 | 描述 |
---|---|
ANSWERED | 已答复 |
DRAFT | 表示此消息为草稿 |
FLAGGED | 用户可根据此自定义语义 |
RECENT | 表示该消息是最新达到文件夹的 |
SEEN | 标记邮件为已读 |
DELETED | 标记邮件为删除 |
// 标记消息为已读
mimeMessage.setFlag(Flags.Flag.SEEN,true);
// 标记消息为已回复
mimeMessage.setFlag(Flags.Flag.ANSWERED,true);
2.2 SpringBoot Mail
本质是对JakartaMail
的封装。
需要引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
2.2.1 快速入门
简单邮件消息
/**
* 也可以通过application.yml配置(host,user,password)
* spring:
* mail:
* host: smtp.163.com
* username: xxx@163.com
* password: 163邮箱授权码
*
*/
@Test
public void testMail() throws MessagingException {
// 1. 创建邮件发送对象
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost(host);
sender.setUsername(from);
sender.setPassword(authCode);
// 2. 构建邮件消息对象
SimpleMailMessage simple_msg = new SimpleMailMessage();
simple_msg.setFrom(from);
simple_msg.setTo(to);
simple_msg.setSubject("[简单文本] Test For SpringBootMail!");
simple_msg.setText("你好!");
// 3. 发送邮件
sender.send(simple_msg);
}
HTML
格式的邮件消息
只需要替换步骤2️⃣中的构建消息部分即可
//========================= 2.2 HTML 格式内容 ====================\\
MimeMessage html_msg = sender.createMimeMessage();
// 消息头:from,to,subject
MimeMessageHelper helper = new MimeMessageHelper(html_msg,"utf-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject("[HTML格式] Test For SpringBootMail!");
// 消息内容
helper.setText("<h1>你好</h1>",true);
HTML
内嵌图片的邮件消息
//========================= 2.3 HTML 内嵌图片 ====================\\
MimeMessage html_img_msg = sender.createMimeMessage();
MimeMessageHelper helper1 = new MimeMessageHelper(html_img_msg, true, "utf-8");
// 消息头:from,to,subject
helper1.setFrom(from);
helper1.setTo(to);
helper1.setSubject("[HTML内嵌图片] Test For SpringBootMail!");
// 消息内容
// 消息内容
helper1.setText("图片:<img src=cid:favicon/>",true);
FileSystemResource resource = new FileSystemResource("src/main/resources/images/favicon.png");
helper1.addInline("favicon",resource);
带附件的邮件消息
//========================= 2.4 附件 ====================\\
MimeMessage attachment_msg = sender.createMimeMessage();
MimeMessageHelper helper2 = new MimeMessageHelper(attachment_msg, true, "utf-8");
// 消息头:from,to,subject
helper2.setFrom(from);
helper2.setTo(to);
helper2.setSubject("[附件] Test For SpringBootMail!");
// 消息内容
helper2.setText("<h1>这是带有附件的HTML格式邮件</h1>",true);
helper2.addAttachment("附件.png",new File("src/main/resources/images/favicon.png"));
示例代码
自定义邮件发送
@Test
public void testMyMail() throws IOException, InterruptedException {
String host = "smtp.163.com";
int port = 25;
// 1. 与邮件服务器建立连接
System.out.println("1. 与邮件服务器建立连接");
Socket socket = new Socket(host, port);
initStream(socket);
// 2. 发送问候
System.out.println("2. 发送问候");
String helo_cmd = "HELO " + host;
sendCmd(helo_cmd.getBytes(StandardCharsets.UTF_8));
if (220 != readResponse()) {
throw new RuntimeException("HELO 命令执行失败!");
}
// 3. 授权命令
System.out.println("3. 授权命令");
String auth_cmd = "auth login";
sendCmd(auth_cmd.getBytes(StandardCharsets.UTF_8));
if (readResponse() != 250) {
throw new RuntimeException("auth login 命令执行失败!");
}
// 4. 验证用户名和密码
System.out.println("4. 验证用户名和密码");
sendCmd(Base64.getEncoder().encode(user.getBytes(StandardCharsets.UTF_8)));
System.out.println(Base64.getEncoder().encodeToString(user.getBytes(StandardCharsets.UTF_8)));
if (readResponse() != 334) {
throw new RuntimeException("用户名输入失败");
}
sendCmd(Base64.getEncoder().encode(password.getBytes(StandardCharsets.UTF_8)));
System.out.println(Base64.getEncoder().encodeToString(password.getBytes(StandardCharsets.UTF_8)));
if (readResponse() != 334) {
throw new RuntimeException("密码输入失败");
}
if (readResponse() != 235) {
throw new RuntimeException("身份认证失败");
}
// 5. 发送邮件
System.out.println("5. 发送邮件");
String mailFrom_cmd = "mail from:<" + user + ">";
sendCmd(mailFrom_cmd.getBytes(StandardCharsets.UTF_8));
if (readResponse() != 250) {
throw new RuntimeException("设置from失败");
}
String rcptTo_cmd = "rcpt to:<" +user +">";
sendCmd(rcptTo_cmd.getBytes(StandardCharsets.UTF_8));
if (readResponse() != 250) {
throw new RuntimeException("设置to失败");
}
// 6. 编写邮件
System.out.println("6. 编写邮件");
sendCmd( "data".getBytes(StandardCharsets.UTF_8) );
if (readResponse() != 354) {
throw new RuntimeException("data命令失败");
}
// 编写邮件内容:邮件头from、to、subject,邮件体 txt StringBuffer msg_cmd = new StringBuffer();
String from = "from:<" + user + ">";
String to = "to:<" + user + ">";
String subject = "subject:Test For MyMail!";
String txt = "Hello MyMail";
msg_cmd.append(from).append(CRLF)
.append(to).append(CRLF)
.append(subject).append(CRLF)
.append(CRLF)
.append(txt)
.append(CRLF).append(".");
System.out.println(msg_cmd);
sendCmd(msg_cmd.toString().getBytes(StandardCharsets.UTF_8));
if (readResponse() != 250) {
throw new RuntimeException("邮件发送失败!");
}
// 关闭资源
serverOutput.close();
serverInput.close();
socket.close();
}
private void initStream(Socket socket) throws IOException {
log.debug("建立连接,isConnected ? {}",socket.isConnected());
log.debug("初始化流对象...");
serverOutput = new BufferedOutputStream(socket.getOutputStream());
serverInput = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
private synchronized int readResponse() throws IOException {
log.debug("读取响应开始...");
String line;
StringBuilder sb = new StringBuilder();
do {
line = serverInput.readLine();
sb.append(line);
sb.append("\n");
} while (isNotLastLine(line));
log.debug("响应内容:{}",sb);
return Integer.parseInt(sb.substring(0,3));
}
private boolean isNotLastLine(String line) {
return line != null && line.length() >= 4 && line.charAt(3) == '-';
}
private synchronized void sendCmd(byte[] cmdBytes) throws IOException {
log.debug("发送命令:{}","`" + new String(cmdBytes) + "`");
serverOutput.write(cmdBytes);
serverOutput.write(CRLF.getBytes(StandardCharsets.UTF_8));
serverOutput.flush();
log.debug("命令发送完毕.");
}
发送邮件(HTML
内嵌图片 + 附件📎)
@Test
public void testMessage() throws Exception{
Properties props = new Properties();
props.setProperty("mail.smtp.host",host);
props.setProperty("mail.smtp.auth","true");
// 1. 创建 Session 对象
Session session = Session.getDefaultInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user,authCode);
}
});
session.setDebug(true);
// 2. 创建消息对象 MimeMessage MimeMessage message = new MimeMessage(session);
// 2.1 设置发件人,收件人以及主题
message.setFrom(user);
Address[] addresses = {new InternetAddress(emailQQ)};
message.setRecipients(Message.RecipientType.TO,addresses); // 收件人
message.setRecipients(Message.RecipientType.BCC,user); // 抄送人
message.setSubject("JavaMail快速入门");
// 2.2 设置消息内容 Content //***************************纯文本|HTML格式内容********************************//
Multipart mp = new MimeMultipart();
MimeBodyPart mbp1 = new MimeBodyPart();
MimeBodyPart mbp2 = new MimeBodyPart();
// HTML格式文本内容
mbp1.setContent("<h1>Hello JavaMail</h1>","text/html;charset=utf-8");
mbp2.setContent("<p>OK!好的</p>","text/html;charset=utf-8");
mp.addBodyPart(mbp1);
mp.addBodyPart(mbp2);
//***************************HTML内嵌图片********************************//
// 图片 MimeBodyPart ==> 用于给文本<img src="">引用
MimeBodyPart imgMbp = new MimeBodyPart();
imgMbp.setDataHandler(new DataHandler(EmailUtil.class.getResource("/imgs/favicon.png")));
imgMbp.setContentID("imgMultipart");
mp.addBodyPart(imgMbp);
// HTML 的图片标签文本,引用上面的 MimeBodyPart即可
MimeBodyPart txtMbp = new MimeBodyPart();
txtMbp.setContent("图片:<img src='cid:imgMultipart' />","text/html;charset=utf-8");
mp.addBodyPart(txtMbp);
//***************************添加附件********************************//
MimeBodyPart img_attachment = new MimeBodyPart();
img_attachment.attachFile("src/main/resources/imgs/favicon.png");
// MimeUtility 对附件中文名称编码 防止乱码
img_attachment.setFileName(MimeUtility.encodeText("附件图片.png"));
mp.addBodyPart(img_attachment);
//******************************************************************//
// 2.3 设置消息内容Content
message.setContent(mp);
// 设置适当的标头
message.saveChanges();
// 3. TransPort 发送消息
Transport.send(message);
}
查看邮件
_注:163
邮箱客户端展示时候无法将多个BodyPart
作为一个整体Multipart
展示,导致,只有第一个部分看着是正文,剩下的都是附件。_
Thymeleaf
模版发送HTML
邮件
Thymeleaf官网
添加依赖
```xml
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
发送邮件
@Test
public void testThymeleaf() throws MessagingException {
//=================== 解析 HTML 返回 String字符串 ===================== // 1. 创建类加载模版解析器 文件存放位置:resources/html/mailTemplate.html
ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
resolver.setPrefix("/html/");
resolver.setSuffix(".html");
// 2. 创建模版引擎处理器
TemplateEngine engine = new TemplateEngine();
engine.setTemplateResolver(resolver);
// 3. 为模版设置数据
Context context = new Context();
context.setVariable("name","clcao");
context.setVariable("image","cid:demo");
// 4. 得到字符串数据
String mailTemplate = engine.process("mailTemplate", context);
System.out.println(mailTemplate);
//=================== 发送邮件 ===================== Properties props = new Properties();
props.setProperty("mail.smtp.host","smtp.163.com");
props.setProperty("mail.smtp.auth","true");
// 获取 Session 对象
Session session = Session.getDefaultInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user, authCode);
}
});
// 创建 MimeMessage 邮件消息
MimeMessage message = new MimeMessage(session);
message.setFrom(user);
message.setRecipient(Message.RecipientType.TO,new InternetAddress(emailQQ));
message.setSubject("Thymeleaf 模版邮件");
//==== 内容部分 ========== Multipart mp = new MimeMultipart();
// 图片
MimeBodyPart imgPart = new MimeBodyPart();
imgPart.setDataHandler(new DataHandler(this.getClass().getResource("/imgs/favicon.png")));
imgPart.setContentID("demo");
mp.addBodyPart(imgPart);
// 内容
MimeBodyPart part = new MimeBodyPart();
part.setContent(mailTemplate,"text/html;charset=utf-8");
mp.addBodyPart(part);
message.setContent(mp);
message.saveChanges();
// 发送消息
Transport.send(message);
}
查看邮件
附录
Part
与Multipart
继承图
文本消息Part
与多文本消息MultiPart
(图片,音频,文件等)
Mime
与Multipart
MIME(Multipurpose Internet Mail Extensions)
多用途互联网邮件扩展,基本由RFC822制定的一系列关于消息的定义,都为MIME
。
MIME标准通过定义数据的类型(也称为MIME类型或内容类型)来允许在单个消息中嵌入多种不同类型的数据。
MIME
不光是邮件SMTP
协议的应用,HTTP
协议也遵循此规范。
在JakartaMail
中,MimeMessage
继承Message
作为邮件消息的标准实现,在MIME
中就定义了Content-Type
标头的定义:
以text/plain;charset=us-ascii
为例,text
就是类型,为文本类型,plain
为subtype
就是子类型为纯文本,charset
就是参数parameter
,格式就是attribute = value
的形式,所以定义为charset=us-ascii
。
Matching of attributes is ALWAYS case-insensitive.(属性匹配都忽略大小写!)请求头其实也是,比如我请求头携带token,通常请求头为:Authorization,但实际上authorization也是可以的,就是在这里规范的。
即使后台获取请求头是:Authorization
依旧也可以获取到
示例
常见的Conteny-Type
基本格式type/subtype
。
文本类型Text
类型 | 描述 |
---|---|
text/plain | 纯文本,没有特定格式 |
text/html | HTML 文档 |
text/css | CSS 样式表 |
text/javascript | text/ecmascript | JavaScript |
text/xml | XML 格式数据 |
text/calender | iCalendar格式,常用于日历数据的交换。 |
text/csv | 逗号分隔值(CSV)文件,常用于表格数据的交换 |
应用程序类型Application
类型 | 描述 |
---|---|
application/json | JSON 数据,一种轻量级的数据交换格式,常用于Web服务中 |
application/xml | XML 数据,与text/xml 类似,但通常用于更复杂的数据交换场景 |
application/pdf | Adobe PDF 文档 |
application/x-www-form-urlencoded | HTML 表单提交的默认编码类型,将表单数据编码为键值对 |
application/msword | Microsoft Word 文档(较旧版本) |
application/zip | ZIP 归档文件 |
application/x-gzip | GZIP 压缩文件 |
application/octer-stream | 二进制流数据,通常用于未知或自定义数据格式 |
媒体类型Multipart
类型 | 描述 | 结构 | RFC 参考 |
---|---|---|---|
multipart/form-data | 主要用于表单数据的编码,特别是当表单中包含文件上传时。它允许将表单数据编码为一条消息发送,其中可以包含文本字段和文件 | 每个部分(part )由边界(boundary )分隔,每个部分都有自己的头部(如Content-Disposition 和Content-Type ),用于描述该部分的数据类型和名称 | RFC 2388 |
multipart/mixed | 用于发送包含多种类型数据的消息,这些数据在逻辑上是独立的,但需要在单个消息中一起发送 | 类似于multipart/form-data ,但通常不包含表单数据,而是多个独立的数据块。每个数据块由边界分隔,并有自己的头部信息 | RFC 2046 |
multipart/alternative | 用于发送相同信息的多种表示形式,以便接收者可以根据自己的能力或偏好选择最合适的表示形式 | 包含多个部分,每个部分都是同一信息的不同表示(如纯文本和HTML 版本)。接收者可以选择最适合自己的部分进行处理 | RFC 2046 |
multipart/byteranges | 用于发送文件的部分内容,通常用于支持HTTP 范围请求(Range requests ) | 每个部分都包含文件的一个或多个字节范围,以及该范围的Content-Type 和Content-Range 头部 | RFC 2616 RFC 7233 |
multipart/related | 用于将一组相互关联的资源封装在单个消息中,这些资源通常具有共同的根资源 | 类似于multipart/mixed ,但每个部分之间具有特定的关系,如包含关系或引用关系。这种类型通常用于表示复合文档或消息 | RFC 2387 |
图片类型Image
类型 | 描述 |
---|---|
image/jpeg | JPEG 图像 |
image/png | PNG 图像 |
image/gif | GIF 图像 |
image/bmp | Windows OS/2 Bitmap Graphics |
image/vnd.microsoft.icon | 图标格式 |
音频/视频类型audio/video
类型 | 描述 |
---|---|
audio/mpeg | MPEG 音频文件 |
audio/mp3 | MP3 音频文件 |
audio/ogg | OGG 音频文件 |
audio/x-ms-wma | WMA 音频文件 |
video/mp4 | MP4 视频文件 |
video/mpeg | MPEG 视频文件 |
video/ogg | OGG 视频文件 |
video/x-ms-wmv | WMV 视频文件 |
video/avi | AVI 视频文件 |