Java收发邮件笔记

1 关于邮箱的前置知识

1.1 概念部分

邮箱服务器和协议是电子邮件系统中不可或缺的两个组成部分,它们共同确保了电子邮件的顺利发送、接收和管理。

1.1.1 服务器和协议

邮箱服务器

比如常见的QQ邮箱,网易邮箱。

邮箱服务器是一种负责电子邮件收发管理的计算机系统。它通过互联网连接到客户端设备,允许用户通过电子邮件与其他用户进行交流和沟通。邮箱服务器不仅负责接收、存储和转发电子邮件,还提供了多种管理和安全功能,如身份认证、邮箱管理、邮件过滤等。

邮箱协议

邮箱协议是指用于电子邮件收发过程中,客户端与服务器之间通信所遵循的规则和标准。常见的邮箱协议包括SMTPPOP3IMAP

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邮箱为例,获取授权码的步骤通常如下:

  1. 登录邮箱:首先,用户需要登录网页版的邮箱账户。
  2. 进入设置:在邮箱首页或设置菜单中,找到并进入账户设置或安全设置等选项。
  3. 开启服务:在账户设置或安全设置中,找到POP3/SMTP/IMAP等服务选项,并开启这些服务。注意,开启这些服务时可能需要进行身份验证,如发送短信验证码等。
  4. 获取授权码:开启服务后,按照页面提示发送短信验证码或进行其他身份验证。验证成功后,系统将生成一个授权码,并显示在页面上。用户需要复制并保存这个授权码,以便在第三方客户端中使用。

1.2.4 注意事项

  • 授权码的唯一性:虽然授权码可以重复使用,但建议用户为每个客户端设置不同的授权码,以提高安全性。
  • 授权码的时效性:授权码可能会因为用户更改密码、关闭服务或邮箱服务提供商的策略调整而失效。因此,用户需要定期检查和更新授权码。
  • 安全性保护:用户应妥善保管授权码,避免泄露给他人。同时,建议定期更换授权码,以提高账户的安全性。

授权码用在Java程序中替代我们在网页上登录诸如163邮箱网址的密码,用户名还是网页的用户名!

1.3 邮件收发基本流程

网络图片

1.3 使用协议收发邮件

此章节内容参考:邮件基本概念及发送方式

协议标准参考:RFC

1.3.1 SMTP协议发送邮件

SMTP命令

1)telnet连接邮件服务器

sh 复制代码
# telnet是一种网络协议,主要用于远程登录到另一台计算机或网络设备(如服务器、路由器等)并执行命令。
telnet smtp.163.com 25

2)helo user_name SMTP协议握手

sh 复制代码
# 输入 ehlo clcao(任意) 并回车,向服务器打招呼,或者命令 helo
ehlo clcao

通过ehlo命令可以看到服务器支持的认证方式

3)认证登录

sh 复制代码
# 身份验证机制(mechanisms):login,其他还有plain、DIGEST-MD5、NTLM、OAuth 2.0等,具体还需要看邮箱服务器支持哪些认证机制
auth login
# 紧接着输入 base64 编码后的用户名
Y2FvY2FpbGlhbmdfc2dwYUAxNjMu29t
# 紧接着输入 base64 编码后的授权码
T0ZQVVhXUVlQU1QSkZSQw==

# 认证成功后的回显
235 Authentication successful

4)编写收发件人邮箱地址

sh 复制代码
# 发件人
mail from:<xxx@163.com>
# 收件人(可以是任意邮箱,包括发件人自己)
rcpt to:<xxx@163.com>

5)编写邮件内容(邮件头,邮件体)

sh 复制代码
# data 后直接回车会提示:
data
# 邮件头:发件人
from:<xxx@163.com>
# 邮件头:收件人
to:<xxx@163.com>
# 邮件头:主题
subject:Test For SMTP
# 邮件头和邮件体需要一行空格

# 邮件体:正文
Hello SMTP!!!
# . 后面再跟一个回车换行结束,即 <CRLF>.<CRLF> 为结束标记
. 

这一步邮件其实已经编写好发送出去了!

6)断开服务器连接

sh 复制代码
quit

1.3.2 POP3协议接收邮件

POP3命令

1)telnet连接邮箱服务器

sh 复制代码
telnet pop.163.com 110

2)登录

sh 复制代码
# 输入用户名,非 Base64 编码
user xxx.163.com

# 输入授权码,非 Base64 编码
OFPUXWQYABBPJFRC

登录后的命令

1)查询概览信息

sh 复制代码
# <CRLF>代表回车换行
stat <CRLF>

2)查询邮件唯一标识符

sh 复制代码
# uidl 2 表示查看序号为2的邮件唯一标识
uidl <id>

3)列出邮件信息

sh 复制代码
# id可选参数,为邮件需号
list <id>

4)获取邮件

sh 复制代码
retr <id>

5)删除邮件

sh 复制代码
dele <id>

6)清除删除标记

sh 复制代码
rset <CRLF>

2 Java收发邮件技术

基于上面的知识,使用Java程序发送邮件,最少需要知道:

  1. 使用哪个邮箱服务器,比如smtp.qq.com
  2. 发件人的用户名和授权码(等同登录网站用的密码)
  3. 邮件
    1. 收件人(至少得知道发送给谁)
    2. 内容(不能为空)

关于使用[1.3 使用协议收发邮件](#关于使用1.3 使用协议收发邮件章节,在断点调试JakartaMail源码时候可以更容易理解里边的一些概念,比如认证器LoginAuthenticator,对应auth login这行命令。)章节,在断点调试JakartaMail源码时候可以更容易理解里边的一些概念,比如认证器LoginAuthenticator,对应auth login这行命令。

Java收发邮件主要框架就是JakartaMail以及对其封装后的spring-boot-starter-mail,但SpringBoot本质技术还是JakartaMail

关于JavaxMailJakartaMail

Javax.*是java标准的一部分,但是没有包含在标准库中,一般属于标准库的扩展。通常属于某个特定领域,不是一般性的api。

Jakarta为雅加达城市,位于爪哇岛,猜测命名同源Java。为JavaEE的后续版本,包括Servlet之前也是javax包下的,现在都迁移到Jakata包下了,所以Java Mail在依赖包这一块也存在区分的,过早版本都是javax.mail现在都是jakarta.mail(推荐)。

1)javax.mail

xml 复制代码
<!--JavaMail基本包-->
<dependency>
	<groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
</dependency>
<!--邮件发送的扩展包-->
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
</dependency>

2)com.sun下的mail

xml 复制代码
<!--使用Sun提供的Email工具包-->
<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
</dependency>

3)Jakarta.mail(推荐)

xml 复制代码
<!--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实现

xml 复制代码
<!--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 快速入门

添加依赖

xml 复制代码
<!--jakarta.mail-->
<dependency>  
  <groupId>com.sun.mail</groupId>  
  <artifactId>jakarta.mail</artifactId>  
  <version>2.0.1</version>  
</dependency>

发送邮件

java 复制代码
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();  
        }  
    }

接收邮件

java 复制代码
@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协议发送邮件](#1.3.1 SMTP协议发送邮件)

自定义发送邮件(不是用JakartaMail框架)

基本思路:

  1. 创建Socket建立与服务器(smtp.163.com 25)的连接
  2. 初始化流信息InputSteam读取响应,OutputStream写命令
  3. 发送命令
    1. HELO 问候
    2. AUTH LOGIN 身份认证
    3. 用户名和密码的Base64输入
    4. MAIL FROM 设置发件人
    5. RECP TO 设置收件人
    6. DATA 开始编写邮件内容
      1. from:发件人
      2. to:收件人
      3. subject:主题
      4. 正文
      5. <CRLF>.<CRLF> 结束DATA邮件编写
  4. 关闭资源
java 复制代码
@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) 获取指定providerurlnametransport
Transport getTransport(URLName url) 指定URLName构建Transport对象
Transport getTransport(Provider provider) 指定provider构建Transport对象
Transport getTransport(Address address) 指定地址构建Transport对象

无论何种方法,最后都会走Transport getTransport(Provider provider, URLName url)

这里涉及两个信息:具体实现ProviderURLName。其中URLName封装的信息有:

除了指定providerurlName的重载方法,其余方法都是根据协议构建Transport,因此只要没有具体指明providerurlName,都遵循以下规则获取协议:

  1. _默认存在配置文件/META-INF/javamail.default.address.map定义在mail.jar包中默认存在一个key-value为:rfc822:smtp
  2. _在类路径下META-INF/javamail.address.map添加配置文件指定协议。
  3. _${java.home}/conf/javamail.address.map下的配置
  4. _如果都没有则会添加一个默认的协议rfc822:smtp

如果拿快速入门来看,其实最终都是默认SMTP协议构建Transport对象,最终也就是:

  1. 默认的Provider实现(SMTPTransport类)
  2. 默认的URLNamenew URLName("smtp", null, -1, null, null, null)

虽然默认的URLName只有协议信息,但是在SMTPTransport构造器实例化对象时候初始化做了很多事情!

SMTPTransport构造器执行

  1. 根据属性mail.smtp.ssl.enable决定默认端口,如果为true则为465,如果为false则为25
  2. 设置一堆属性,具体参考下表
  3. 创建默认的认证器,用于身份认证机制
    1. LoginAuthenticator
    2. PlainAuthenticator
    3. DigestMD5Authenticator
    4. NtlmAuthenticator
    5. OAuth2Authenticator

由于SMTPTransport extends Transport extends Service,所以实例化SMTPTransport之前会执行Servie的构造。

Servcie构造器执行

  1. 根据属性mail.smtp.host获取host信息
    1. 如果没有则根据属性mail.host获取
  2. 根据属性mail.smtp.user获取user信息
    1. 如果没有则根据属性mail.user获取
  3. 构建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) 向指定的地址发送消息

具体规则为:

  1. _无论使用哪个静态重载方法,最后都走实例方法static void send(Message msg, Address[] addresses, String user, String password
  2. _如果msg存在session则获取msg中定义的session创建Transport对象
  3. _如果不存在则,创建默认的Transport对象(Session.getDefaultInstance(System.getProperties(), null)

所以,此处代码可以变通

java 复制代码
// 由于创建默认的 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

具体获取规则如下:

  1. 如果Transport.send()或者transport.sendMessage()没有指定host,user,password,则从URLName获取
  2. 如果为空则从属性mail.smtp.host|user获取。(有点无语,因为Service只要加载就会读取这些属性封装到URLName上)
  3. 如果还为空则从属性mail.host|user获取
  4. 如果user还为空,直接读取系统属性System.getProperty("user.name")
  5. 如果密码为空,则从session.getPasswordAuthentication获取
java 复制代码
// 如果没有手动构建URLName 对象,那么密码将从这里获取
session.setPasswordAuthentication(transport.getURLName(),new PasswordAuthentication(fromUser,authCode));

无论是否获取到这些信息,最终都将执行连接的真正方法:protocolConnect(host, port, user, password)

protocolConnect执行

  1. 获取属性mail.smtp.auth,判读是否需要身份认证,默认为false
  2. 获取属性mail.smtp.ehlo,如果为false将默认执行helo问候,默认为true这个会导致服务器响应内容为空,导致身份认证失败,不推荐!
  3. 构建Socket连接并执行ehlo或者helo问候
  4. 如果设置了属性mail.smtp.authtrue,或者userpassword都不为空则进行身份认证
    1. 发送命令AUTH LOGIN
    2. 发送用户名和密码

也可以直接构建socket连接

java 复制代码
SMTPTransport transport = ((SMTPTransport) session.getTransport());  
transport.connect(new Socket(emailHost,25));

到这里相当于完成了命令发送邮件的两个步骤:telnet smtp.163.comhelo hello 以及auth login!。

连接完成后,才是发送消息,因此,如果自己调用实例方法sendMessage(),一定需要先连接。

java 复制代码
transport.connect();  
transport.sendMessage(msg,msg.getAllRecipients());

sendMessage()执行

  1. mailFrom()设置发件人
  2. rcpt() 设置收件人
  3. 写消息
Message

参考章节The Message Class

Message类定义了消息的属性和内容,属性包括收发件人的地址信息,同时也定义了内容结构,比如HTML格式,包括Content-type,其中内容由DataHandler类表示。

一个消息对象应该包括:

  • 收发件人地址信息
  • 消息头属性(Content-Type
  • 消息体内容
  • 消息状态(是否已读)

MimeMessage

1.4.2 常用API

属性设置相关

java 复制代码
// 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

java 复制代码
# 保存邮件
public void saveChanges() throws MessagingException;

为消息对象产生字节流

java 复制代码
public void writeTo(OutputStream os) throws IOException, MessagingException;

发送消息时候就是写数据,使用message.writeTo()

Part 接口

MessageBodyPart都实现了此接口,通用的Part抽象,定义了一系列Header属性,并提供了getter,setter方法。

消息属性

即定义在Message类上扩展的属性,包括:

  • 发件人from
  • 收件人recipitents
  • 发件时间received date
  • 主题subject
  • 标记falg

Content-Type属性

contentType属性按照MIME类型规范(RFC 2045)指定内容的数据类型。MIME类型由声明内容一般类型的主类型和指定内容特定格式的子类型组成。MIME类型还包括一组可选的特定类型参数。

Address

封装电子邮件地址的类。

BodyPart

消息体的部分,一个Multipart可以包含多个BodyPartMultipart作为BodyPart的容器

MultiPart

要求Content-Typemultipart类型,作为BodyPart的容器。

MessageMultiPart

常见的MultiPart类型

Flag

消息的状态信息,包括邮件是否已读,是否删除,是否答复等。

Flag类型 描述
ANSWERED 已答复
DRAFT 表示此消息为草稿
FLAGGED 用户可根据此自定义语义
RECENT 表示该消息是最新达到文件夹的
SEEN 标记邮件为已读
DELETED 标记邮件为删除
java 复制代码
// 标记消息为已读
mimeMessage.setFlag(Flags.Flag.SEEN,true);  
// 标记消息为已回复
mimeMessage.setFlag(Flags.Flag.ANSWERED,true);

2.2 SpringBoot Mail

本质是对JakartaMail的封装。

需要引入依赖:

xml 复制代码
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-mail</artifactId>  
</dependency>

2.2.1 快速入门

简单邮件消息
java 复制代码
/**  
     * 也可以通过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️⃣中的构建消息部分即可

java 复制代码
//========================= 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内嵌图片的邮件消息
java 复制代码
//========================= 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);
带附件的邮件消息
java 复制代码
//========================= 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"));

示例代码

自定义邮件发送
java 复制代码
@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内嵌图片 + 附件📎)
java 复制代码
@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 复制代码
```xml
<dependency>
  <groupId>org.thymeleaf</groupId>
  <artifactId>thymeleaf</artifactId>
  <version>3.1.2.RELEASE</version>
</dependency>

发送邮件

java 复制代码
@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);  
}

查看邮件


附录

PartMultipart继承图

文本消息Part与多文本消息MultiPart(图片,音频,文件等)

MimeMultipart

MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展,基本由RFC822制定的一系列关于消息的定义,都为MIME

MIME标准通过定义数据的类型(也称为MIME类型或内容类型)来允许在单个消息中嵌入多种不同类型的数据。

MIME不光是邮件SMTP协议的应用,HTTP协议也遵循此规范。

JakartaMail中,MimeMessage继承Message作为邮件消息的标准实现,在MIME中就定义了Content-Type标头的定义:

text/plain;charset=us-ascii为例,text就是类型,为文本类型,plainsubtype就是子类型为纯文本,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-DispositionContent-Type),用于描述该部分的数据类型和名称 RFC 2388
multipart/mixed 用于发送包含多种类型数据的消息,这些数据在逻辑上是独立的,但需要在单个消息中一起发送 类似于multipart/form-data,但通常不包含表单数据,而是多个独立的数据块。每个数据块由边界分隔,并有自己的头部信息 RFC 2046
multipart/alternative 用于发送相同信息的多种表示形式,以便接收者可以根据自己的能力或偏好选择最合适的表示形式 包含多个部分,每个部分都是同一信息的不同表示(如纯文本和HTML版本)。接收者可以选择最适合自己的部分进行处理 RFC 2046
multipart/byteranges 用于发送文件的部分内容,通常用于支持HTTP范围请求(Range requests 每个部分都包含文件的一个或多个字节范围,以及该范围的Content-TypeContent-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视频文件
相关推荐
Source.Liu15 分钟前
【用Rust写CAD】第二章 第四节 函数
开发语言·rust
monkey_meng15 分钟前
【Rust中的迭代器】
开发语言·后端·rust
余衫马18 分钟前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng21 分钟前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
七星静香22 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员23 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU24 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie627 分钟前
在IDEA中使用Git
java·git
小白学大数据30 分钟前
正则表达式在Kotlin中的应用:提取图片链接
开发语言·python·selenium·正则表达式·kotlin
VBA633732 分钟前
VBA之Word应用第三章第三节:打开文档,并将文档分配给变量
开发语言