Apache Commons Email 快速入门与集成示例
Commons Email 1.6.0 快速入门:Maven 集成、SimpleEmail 纯文本、MultiPartEmail 附件、HtmlEmail 内联图片、ImageHtmlEmail 自动内联、综合示例与真实 MIME 报文。
本地 SMTP 调试服务器:打印完整 SMTP 会话,并把每封邮件落盘为 eml
目标:十分钟内跑通 Apache Commons Email 1.6.0 的核心场景——Maven 集成、纯文本发信、带附件、HTML 正文与内联图片、模板自动内联、含抄送密送回复地址的综合邮件,并逐条解析发信服务器端抓到的真实 MIME 报文。全部示例已在 Ubuntu 22.04(JDK 17 + Maven)实测通过。
一、环境信息
本章先确认运行环境,再准备一台可接收邮件的本地 SMTP 调试服务器,后续所有示例都向它发信并把真实报文落盘比对。Commons Email 1.6.0 字节码目标为 Java 8,示例统一用 JDK 17 编译运行。验证环境:
java -version 2>&1 | head -1
mvn -v | head -1
实测回显(Ubuntu 22.04):
openjdk version "17.0.20.1" 2026-08-18
Apache Maven 3.6.3
💡 技术拓展:动手前先分清「邮件协议」和「Java 邮件库」两个层面。协议是客户端与邮件服务器之间收发信的标准(SMTP/SMTPS/IMAP/POP3);库是帮你按这些协议对话的编程接口。常见协议与 Java 库的对比:
| 名称 | 归属 | 作用 | 典型端口 |
|---|---|---|---|
| SMTP | 协议 | 客户端把邮件交给发信服务器 | 25(明文)、587(提交) |
| SMTPS | 协议 | 从连接起就用 TLS 加密的 SMTP | 465 |
| STARTTLS | 协议扩展 | 明文连接上协商升级为加密 | 25/587 |
| IMAP | 协议 | 客户端从服务器读取/保持邮件 | 143(明文)、993(TLS) |
| POP3 | 协议 | 客户端从服务器下载邮件 | 110(明文)、995(TLS) |
| JavaMail(jakarta.mail) | 库 | 协议底层实现,Commons Email 的依赖 | 不直接暴露 |
| Commons Email | 库 | 在 JavaMail 之上封装发信 API | 不直接暴露 |
| Spring Mail | 库 | Spring 体系封装,底层同样用 JavaMail | 不直接暴露 |
本文示例全部走 SMTP 明文(25 端口)发信,重点展示 Commons Email 的报文组装能力;加密与认证参数见《Apache Commons Email 配置参数详解》。
1.1 准备本地 SMTP 调试服务器
集成示例需要一台能接收邮件的 SMTP 服务器。本文实测使用一台监听 25 端口的调试服务器,它把收到的每封邮件原样落盘为 .eml 文件,便于核对真实 MIME 报文。用 Python 标准库实现,无第三方依赖:
#!/usr/bin/env python3
import socketserver, os, sys
OUTDIR = "/opt/mail-lab/captured"
os.makedirs(OUTDIR, exist_ok=True)
counter = 0
class Handler(socketserver.StreamRequestHandler):
def log(self, s):
sys.stdout.write(s + "\n"); sys.stdout.flush()
def handle(self):
global counter
self.log("=== NEW CONNECTION %s:%s ===" % self.client_address)
self.wfile.write(b"220 mail-lab.local ESMTP\r\n")
data_mode = False
msg_lines = []
while True:
line = self.rfile.readline()
if not line:
break
text = line.decode("utf-8", "replace").rstrip("\r\n")
if data_mode:
if text == ".":
data_mode = False
counter += 1
fn = os.path.join(OUTDIR, "captured-%03d.eml" % counter)
with open(fn, "w") as f:
f.write("\r\n".join(msg_lines) + "\r\n")
self.wfile.write(b"250 OK, message accepted\r\n")
self.log("<<< 250 OK, message accepted (saved %s)" % fn)
msg_lines = []
continue
msg_lines.append(text)
continue
self.log(">>> " + text)
up = text.upper()
if up.startswith("EHLO"):
self.wfile.write(b"250-mail-lab.local\r\n250-SIZE 10485760\r\n250-8BITMIME\r\n250 AUTH LOGIN PLAIN\r\n")
self.log("<<< 250-mail-lab.local | 250-SIZE 10485760 | 250-8BITMIME | 250 AUTH LOGIN PLAIN")
elif up.startswith("HELO"):
self.wfile.write(b"250 mail-lab.local\r\n"); self.log("<<< 250 mail-lab.local")
elif up.startswith("MAIL FROM"):
self.wfile.write(b"250 2.1.0 Ok sender\r\n"); self.log("<<< 250 2.1.0 Ok sender")
elif up.startswith("RCPT TO"):
self.wfile.write(b"250 2.1.5 Ok recipient\r\n"); self.log("<<< 250 2.1.5 Ok recipient")
elif up == "DATA":
self.wfile.write(b"354 End data with <CR><LF>.<CR><LF>\r\n"); self.log("<<< 354 End data with <CR><LF>.<CR><LF>")
data_mode = True
elif up.startswith("QUIT"):
self.wfile.write(b"221 2.0.0 Bye\r\n"); self.log("<<< 221 2.0.0 Bye"); break
elif up.startswith("RSET") or up.startswith("NOOP"):
self.wfile.write(b"250 2.0.0 Ok\r\n"); self.log("<<< 250 2.0.0 Ok")
else:
self.wfile.write(b"502 Command not implemented\r\n"); self.log("<<< 502 Command not implemented")
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 25
srv = Server(("0.0.0.0", port), Handler)
print("SMTP debug server listening on 0.0.0.0:%d" % port, flush=True)
srv.serve_forever()
服务器逻辑:EHLO 回 250 并广告 AUTH LOGIN PLAIN;MAIL FROM/RCPT TO 各回 250;收到 DATA 后进入数据模式,逐行累积直到单独一行 .,把整封报文写入 /opt/mail-lab/captured/captured-NNN.eml。启动它:
mkdir -p /opt/mail-lab
python3 /opt/mail-lab/smtp_debug.py 25
实测回显:
SMTP debug server listening on 0.0.0.0:25
在另一个终端确认 25 端口已在监听:
netstat -lnt | grep ":25 "
实测回显:
tcp 0 0 0.0.0.0:25 0.0.0.0:* LISTEN
有这一行即表示发信目标就绪。后文每发一封邮件,服务器都会把报文落到 captured/ 目录,我们用它来核对真实 MIME 结构。
二、Maven 集成
创建项目骨架并声明依赖:
mkdir -p /opt/mail-demo/src/main/java/net/quzuo/demo
cd /opt/mail-demo
该命令无输出。写入 pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.quzuo.demo</groupId>
<artifactId>mail-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-email</artifactId>
<version>1.6.0</version>
</dependency>
</dependencies>
</project>
逐项说明:maven.compiler.source/target 指定按 Java 17 语法编译;project.build.sourceEncoding 强制源码按 UTF-8 读取;dependencies 只声明 commons-email 一个依赖,底层 JavaMail 会作为传递依赖自动引入。验证依赖解析结果:
mvn dependency:tree
实测回显(节选,实测首尾):
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ mail-demo ---
[INFO] net.quzuo.demo:mail-demo:jar:1.0.0
[INFO] \- org.apache.commons:commons-email:jar:1.6.0:compile
[INFO] \- com.sun.mail:jakarta.mail:jar:1.6.7:compile
[INFO] \- com.sun.activation:jakarta.activation:jar:1.2.1:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
依赖树确认两个传递依赖都是 compile 作用域:com.sun.mail:jakarta.mail:1.6.7(SMTP/IMAP/POP3 协议实现)与 com.sun.activation:jakarta.activation:1.2.1(附件与内联资源的 MIME 编码)。注意这两个坐标虽叫 jakarta,实际包名仍是 javax.mail/javax.activation。
准备两个示例资源——一个纯文本附件与一张最小 PNG 图片:
cd /opt/mail-demo
printf "Apache Commons Email 附件测试文件\n生成时间: 2026-10-14\n" > lab.txt
python3 -c "
import base64
png=base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAXklEQVR4nO3QMQEAAAgDoJnc6hgF00gB7nRAbwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4NfAFAAABm8cZ4gAAAABJRU5ErkJggg==')
open('/opt/mail-demo/logo.png','wb').write(png)
"
ls -l lab.txt logo.png
实测回显:
-rw-r--r-- 1 root root 65 Oct 13 17:09 lab.txt
-rw-r--r-- 1 root root 160 Oct 13 17:09 logo.png
两个资源就绪:lab.txt 用于附件示例,logo.png 用于内联图片示例。下面每章的示例类都写到 src/main/java/net/quzuo/demo/ 下,用 mvn -q compile exec:java 编译运行。
三、SimpleEmail:纯文本发信
SimpleEmail 是家族里最简的入口,只需发件人、收件人、主题与正文。写一个最小示例 SimpleDemo.java:
package net.quzuo.demo;
import org.apache.commons.mail.SimpleEmail;
public class SimpleDemo {
public static void main(String[] args) throws Exception {
SimpleEmail email = new SimpleEmail();
email.setHostName("127.0.0.1");
email.setSmtpPort(25);
email.setCharset("UTF-8");
email.setFrom("sender@quzuo.net");
email.setSubject("Commons Email quick start");
email.setMsg("hello commons email, sent from Ubuntu 22.04");
email.addTo("receiver@quzuo.net");
String id = email.send();
System.out.println("SimpleEmail 发送成功, Message-ID: " + id);
}
}
要点逐条说明:setHostName 是发信服务器地址;setSmtpPort(25) 走明文 SMTP;setCharset("UTF-8") 决定正文编码;setFrom/addTo 分别设置发件人与收件人(可多次 addTo 加多个收件人);send() 返回服务器分配的 Message-ID。运行:
cd /opt/mail-demo
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.SimpleDemo
实测回显:
SimpleEmail 发送成功, Message-ID: <1165379185.0.1791911636484@localhost>
服务器端抓到并落盘的报文(captured-028.eml 完整内容):
Date: Tue, 13 Oct 2026 17:13:41 +0000 (GMT)
From: sender@quzuo.net
To: receiver@quzuo.net
Message-ID: <1165379185.0.1791911636484@localhost>
Subject: Commons Email quick start
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
hello commons email, sent from Ubuntu 22.04
报文可读出三条关键信息:Content-Type: text/plain; charset=UTF-8 是 setCharset("UTF-8") 的直接效果;Content-Transfer-Encoding: 7bit 表示正文全是 ASCII 明文字节,无需额外编码;Message-ID 的域名部分为 @localhost(由发信主机名推导),与 send() 返回值一致。
> 💡 技术拓展: Commons Email 的家族类在 Email 抽象基类上层层加能力,选型时按需要挑最简的那个即可:
| 类 | 继承自 | 增加的能力 | 适用场景 |
|---|---|---|---|
| SimpleEmail | setMsg(String) | 纯文本正文 | |
| MultiPartEmail | addPart/attach(正文+附件) | 正文外挂文件 | |
| HtmlEmail | MultiPartEmail | setHtmlMsg/setTextMsg/embed(HTML+内联图) | 富文本邮件 |
| ImageHtmlEmail | HtmlEmail | 自动内联模板里的本地图片 | HTML 模板批量发信 |
| EmailAttachment | 独立类 | setPath/setName/setDisposition | 附件描述对象 |
后文依次演示 MultiPartEmail、HtmlEmail、ImageHtmlEmail,最后给一个综合示例。
四、MultiPartEmail:带附件
MultiPartEmail 在正文之外支持挂附件。示例把 lab.txt 作为普通附件发出:
package net.quzuo.demo;
import org.apache.commons.mail.EmailAttachment;
import org.apache.commons.mail.MultiPartEmail;
public class MultiPartEmailDemo {
public static void main(String[] args) throws Exception {
EmailAttachment attachment = new EmailAttachment();
attachment.setPath("/opt/mail-demo/lab.txt");
attachment.setDisposition(EmailAttachment.ATTACHMENT);
attachment.setDescription("Commons Email 测试附件");
attachment.setName("lab.txt");
MultiPartEmail email = new MultiPartEmail();
email.setHostName("127.0.0.1");
email.setSmtpPort(25);
email.setCharset("UTF-8");
email.setFrom("sender@quzuo.net");
email.setSubject("MultiPartEmail with attachment");
email.setMsg("正文:这是一个带附件的多部分邮件。");
email.addTo("receiver@quzuo.net");
email.attach(attachment);
String id = email.send();
System.out.println("MultiPartEmail 发送成功, Message-ID: " + id);
}
}
要点逐条说明:EmailAttachment.setPath 指定本地文件;setDisposition(EmailAttachment.ATTACHMENT) 把它标记为可下载附件(另一种取值 INLINE 表示随正文内联);setName 是收件人看到的文件名;attach(attachment) 把它挂到邮件上。运行:
cd /opt/mail-demo
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.MultiPartEmailDemo
实测回显:
MultiPartEmail 发送成功, Message-ID: <783286238.1.1791911384465@localhost>
服务器端落盘的报文(captured-022.eml 完整内容):
Date: Tue, 13 Oct 2026 17:09:29 +0000 (GMT)
From: sender@quzuo.net
To: receiver@quzuo.net
Message-ID: <783286238.1.1791911384465@localhost>
Subject: MultiPartEmail with attachment
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_Part_0_1706377736.1791911369256"
------=_Part_0_1706377736.1791911369256
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64
5q2j5paH77ya6L+Z5piv5LiA5Liq5bim6ZmE5Lu255qE5aSa6YOo5YiG6YKu5Lu244CC
------=_Part_0_1706377736.1791911369256
Content-Type: text/plain; charset=UTF-8; name=lab.txt
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment; filename=lab.txt
Content-Description: =?UTF-8?Q?Commons_Email_=E6=B5=8B=E8=AF=95=E9=99=84=E4=BB=B6?=
Apache Commons Email =E9=99=84=E4=BB=B6=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6
=E7=94=9F=E6=88=90=E6=97=B6=E9=97=B4: 2026-10-14
------=_Part_0_1706377736.1791911369256--
报文是多部分结构,逐段解读:顶层 Content-Type: multipart/mixed 声明「正文 + 附件」混排,boundary 是分隔各段的边界串;第一段是正文(Content-Transfer-Encoding: base64,因为正文含中文,base64 解码即 正文:这是一个带附件的多部分邮件。);第二段是附件,Content-Disposition: attachment; filename=lab.txt 就是 setDisposition 与 setName 的效果,Content-Description 是显示名被 MIME 编码后的形态;附件体用 quoted-printable 编码,=E9=99=84 之类即中文字节的转义。
五、HtmlEmail:HTML 正文与内联图片
HtmlEmail 支持 HTML 正文,并可通过 embed 把本地图片作为内联资源(而非附件)嵌入,HTML 里用 cid: 引用。示例同时设置纯文本回退:
package net.quzuo.demo;
import java.io.File;
import org.apache.commons.mail.HtmlEmail;
public class HtmlEmailDemo {
public static void main(String[] args) throws Exception {
HtmlEmail email = new HtmlEmail();
email.setHostName("127.0.0.1");
email.setSmtpPort(25);
email.setCharset("UTF-8");
email.setFrom("sender@quzuo.net");
email.setSubject("HtmlEmail inline image");
email.setTextMsg("您的邮箱不支持 HTML,这是一份纯文本回退。");
email.addTo("receiver@quzuo.net");
String cid = email.embed(new File("/opt/mail-demo/logo.png"), "quzuo-logo");
email.setHtmlMsg("<html><body><h2>Hello Commons Email</h2>"
+ "<img src=\"cid:" + cid + "\"/>"
+ "<p>内联图片已嵌入。</p></body></html>");
String id = email.send();
System.out.println("HtmlEmail 发送成功, cid: " + cid);
System.out.println("Message-ID: " + id);
}
}
要点逐条说明:embed(File) 与 embed(File, cid) 接收本地文件,返回内容标识符 cid(这里显式传入 quzuo-logo,返回的 cid 就是它);setHtmlMsg 是 HTML 正文,里面用 <img src="cid:quzuo-logo"> 引用内联图;setTextMsg 是给不支持 HTML 的客户端用的纯文本回退。注意 embed(String, String) 的重载第一参是 URL 而非本地路径,本地文件必须用 embed(File, ...)。运行:
cd /opt/mail-demo
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.HtmlEmailDemo
实测回显:
HtmlEmail 发送成功, cid: quzuo-logo
Message-ID: <1058025095.3.1791911409814@localhost>
服务器端落盘的报文(captured-023.eml 完整内容):
Date: Tue, 13 Oct 2026 17:09:54 +0000 (GMT)
From: sender@quzuo.net
To: receiver@quzuo.net
Message-ID: <1058025095.3.1791911409814@localhost>
Subject: HtmlEmail inline image
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_Part_0_205797316.1791911394674"
------=_Part_0_205797316.1791911394674
Content-Type: multipart/related;
boundary="----=_Part_1_935044096.1791911394675"
------=_Part_1_935044096.1791911394675
Content-Type: multipart/alternative;
boundary="----=_Part_2_396180261.1791911394675"
------=_Part_2_396180261.1791911394675
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64
5oKo55qE6YKu566x5LiN5pSv5oyBIEhUTUzvvIzov5nmmK/kuIDku73nuq/mlofmnKzlm57pgIDj
gII=
------=_Part_2_396180261.1791911394675
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
<html><body><h2>Hello Commons Email</h2><img src=3D"cid:quzuo-logo"/><p>=E5=
=86=85=E8=81=94=E5=9B=BE=E7=89=87=E5=B7=B2=E5=B5=8C=E5=85=A5=E3=80=82</p></=
body></html>
------=_Part_2_396180261.1791911394675--
------=_Part_1_935044096.1791911394675
Content-Type: image/png; name=logo.png
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename=logo.png
Content-ID: <quzuo-logo>
iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAXklEQVR4nO3QMQEAAAgDoJnc6hgF
00gB7nRAbwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4NfAFAAABm8cZ4gAAAABJRU5ErkJggg==
------=_Part_1_935044096.1791911394675--
------=_Part_0_205797316.1791911394674--
这段报文是三层嵌套,从外到内逐层解读:最外层 multipart/mixed(承载正文与其他部分);第二层 multipart/related(把 HTML 与它的内联图片绑定在一起);第三层 multipart/alternative(同一内容的两种表示,纯文本段与 HTML 段并列,客户端自行选一种显示);multipart/related 里除 HTML 外还有 Content-ID: <quzuo-logo> 的图片段,Content-Disposition: inline 说明它是内联资源。HTML 段的 src=3D"cid:quzuo-logo" 中的 =3D 是等号 = 的 quoted-printable 转义。
六、ImageHtmlEmail:模板自动内联
ImageHtmlEmail 在 HtmlEmail 基础上更省事:HTML 里直接写本地图片路径,库会自动把图片解析并内联,无需手工 embed。它需要一个 DataSourceResolver 告诉它去哪个目录找图片:
package net.quzuo.demo;
import java.io.File;
import org.apache.commons.mail.ImageHtmlEmail;
import org.apache.commons.mail.resolver.DataSourceFileResolver;
public class ImageHtmlEmailDemo {
public static void main(String[] args) throws Exception {
ImageHtmlEmail email = new ImageHtmlEmail();
email.setHostName("127.0.0.1");
email.setSmtpPort(25);
email.setCharset("UTF-8");
email.setDataSourceResolver(new DataSourceFileResolver(new File("/opt/mail-demo")));
email.setFrom("sender@quzuo.net");
email.setSubject("ImageHtmlEmail auto inline");
email.setHtmlMsg("<html><body><h2>ImageHtmlEmail 模板邮件</h2>"
+ "<img src=\"logo.png\"/>"
+ "<p>模板中的本地图片已被自动内联。</p></body></html>");
email.addTo("receiver@quzuo.net");
String id = email.send();
System.out.println("ImageHtmlEmail 发送成功, Message-ID: " + id);
}
}
要点逐条说明:setDataSourceResolver(new DataSourceFileResolver(基准目录)) 让库以该目录为基准解析 HTML 里的相对路径;setHtmlMsg 里直接写 <img src="logo.png"/>,发送时库自动读出 /opt/mail-demo/logo.png 并替换为 cid: 引用(cid 由库随机生成)。运行:
cd /opt/mail-demo
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.ImageHtmlEmailDemo
实测回显:
ImageHtmlEmail 发送成功, Message-ID: <1612799726.2.1791911435134@localhost>
服务器端落盘的报文(captured-024.eml 完整内容):
Date: Tue, 13 Oct 2026 17:10:20 +0000 (GMT)
From: sender@quzuo.net
To: receiver@quzuo.net
Message-ID: <1612799726.2.1791911435134@localhost>
Subject: ImageHtmlEmail auto inline
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_Part_0_443308702.1791911419984"
------=_Part_0_443308702.1791911419984
Content-Type: multipart/related;
boundary="----=_Part_1_1560911714.1791911419984"
------=_Part_1_1560911714.1791911419984
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
<html><body><h2>ImageHtmlEmail =E6=A8=A1=E6=9D=BF=E9=82=AE=E4=BB=B6</h2><im=
g src=3D"cid:ddyzrgutuj"/><p>=E6=A8=A1=E6=9D=BF=E4=B8=AD=E7=9A=84=E6=9C=AC=
=E5=9C=B0=E5=9B=BE=E7=89=87=E5=B7=B2=E8=A2=AB=E8=87=AA=E5=8A=A8=E5=86=85=E8=
=81=94=E3=80=82</p></body></html>
------=_Part_1_1560911714.1791911419984
Content-Type: image/png; name=logo.png
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename=logo.png
Content-ID: <ddyzrgutuj>
iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAXklEQVR4nO3QMQEAAAgDoJnc6hgF
00gB7nRAbwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4NfAFAAABm8cZ4gAAAABJRU5ErkJggg==
------=_Part_1_1560911714.1791911419984--
------=_Part_0_443308702.1791911419984--
对比上章的 HtmlEmail 报文可见差异:这里没有 multipart/alternative(因为只设置了 HTML 正文、没设纯文本回退),结构是 multipart/mixed → multipart/related → (HTML + 图片);HTML 段里的 src=3D"cid:ddyzrgutuj" 说明 logo.png 已被自动替换成库随机生成的 cid ddyzrgutuj,对应图片段的 Content-ID: <ddyzrgutuj>。
七、综合示例:抄送、密送、回复与自定义头
最后把附件、内联图片、抄送(Cc)、密送(Bcc)、回复地址(Reply-To)与自定义头组合到一封邮件里:
package net.quzuo.demo;
import org.apache.commons.mail.EmailAttachment;
import org.apache.commons.mail.HtmlEmail;
public class AdvancedDemo {
public static void main(String[] args) throws Exception {
EmailAttachment attachment = new EmailAttachment();
attachment.setPath("/opt/mail-demo/lab.txt");
attachment.setDisposition(EmailAttachment.ATTACHMENT);
attachment.setName("lab.txt");
HtmlEmail email = new HtmlEmail();
email.setHostName("127.0.0.1");
email.setSmtpPort(25);
email.setCharset("UTF-8");
email.setFrom("sender@quzuo.net", "Quzuo 发信机器人");
email.addTo("receiver@quzuo.net", "收件人");
email.addCc("cc@quzuo.net");
email.addBcc("bcc@quzuo.net");
email.addReplyTo("support@quzuo.net", "支持团队");
email.addHeader("X-Campaign", "commons-email-lab");
email.setSubject("Commons Email 综合示例");
email.setHtmlMsg("<html><body><h2>综合示例</h2>"
+ "<img src=\"cid:quzuo-logo\"/>"
+ "<p>包含抄送、密送、回复地址与自定义头。</p></body></html>");
email.setTextMsg("纯文本回退内容。");
String cid = email.embed(new java.io.File("/opt/mail-demo/logo.png"), "quzuo-logo");
email.attach(attachment);
String id = email.send();
System.out.println("综合示例发送成功, cid: " + cid + ", Message-ID: " + id);
}
}
要点逐条说明:addTo/addCc/addBcc/addReplyTo 分别追加收件人、抄送、密送、回复地址,都支持「地址 + 显示名」两参形式;addHeader 写入任意自定义头(如 X-Campaign);embed 加内联图、attach 挂附件可同时使用(内联图在 multipart/related 层、附件在 multipart/mixed 层)。运行:
cd /opt/mail-demo
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.AdvancedDemo
实测回显:
综合示例发送成功, cid: quzuo-logo, Message-ID: <1156060786.3.1791911460480@localhost>
服务器端落盘报文的首部(captured-025.eml 节选,实测首尾):
Date: Tue, 13 Oct 2026 17:10:45 +0000 (GMT)
From: =?UTF-8?Q?Quzuo_=E5=8F=91=E4=BF=A1=E6=9C=BA=E5=99=A8=E4=BA=BA?=
<sender@quzuo.net>
Reply-To: =?UTF-8?B?5pSv5oyB5Zui6Zif?= <support@quzuo.net>
To: =?UTF-8?B?5pS25Lu25Lq6?= <receiver@quzuo.net>
Cc: cc@quzuo.net
Message-ID: <1156060786.3.1791911460480@localhost>
Subject: =?UTF-8?Q?Commons_Email_=E7=BB=BC=E5=90=88=E7=A4=BA=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_Part_0_205797316.1791911445294"
X-Campaign: commons-email-lab
报文首部逐项对应源码:setFrom(email, name) 的显示名「Quzuo 发信机器人」被 MIME 编码为 =?UTF-8?Q?...?=(Q 编码);addReplyTo 写进 Reply-To;addTo 的显示名「收件人」被编为 =?UTF-8?B?...?=(B 编码,同内容不同编码方式);addCc 写进 Cc;addHeader("X-Campaign", ...) 原样写进同名头。报文里看不到 Bcc——密送地址只出现在 SMTP 会话的 RCPT TO 里,不写进报文头,这正是密送的设计意图:收件人之间互相看不到密送对象。
综合报文的正文部分是三层嵌套的 multipart/mixed → multipart/related → multipart/alternative:alternative 层放纯文本回退与 HTML,related 层放 HTML 与内联图片 quzuo-logo,最外层 mixed 再挂上 lab.txt 附件段。这与第五章 HtmlEmail 的结构一致,只是额外多了一个附件段。
八、参考链接
- Apache Commons Email 官方主页:https://commons.apache.org/proper/commons-email/
- 官方用户指南(Simple/MultiPart/Html/Image 用法):https://commons.apache.org/proper/commons-email/userguide.html
- 1.6.0 官方 javadoc(HtmlEmail、EmailAttachment 等):https://commons.apache.org/proper/commons-email/apidocs/index.html
- Maven 中央仓库 1.6.0(pom 与 jar):https://repo1.maven.org/maven2/org/apache/commons/commons-email/1.6.0/
- JavaMail 项目(传递依赖 jakarta.mail):https://eclipse-ee4j.github.io/mail/
- MIME 多部分类型说明(RFC 2046,multipart/mixed、related、alternative):https://www.rfc-editor.org/rfc/rfc2046