Apache Commons Email 常用 CLI 命令与工具详解
实测 Apache Commons Email 1.6.0 命令行用法:jar 无 Main-Class、java -cp 启动、javap 查看家族类 API、家族类逐个实操,并用 netstat、openssl 诊断发信端口。
Apache Commons Email 常用 CLI 命令与工具详解
目标:搞清楚 Apache Commons Email 1.6.0 到底能怎么「在命令行用」——jar 清单为何没有入口、
java -cp启动与javap查 API、家族类逐个实操,以及用netstat、openssl诊断发信端口。全部命令与回显已在 Ubuntu 22.04(JDK 17)实测通过。
一、环境信息
本章所有命令行操作基于以下环境。Commons Email 是 jar 库,无常驻服务,其「命令行能力」只有三种形态:java -cp 直接运行演示程序、javap 反编译查看类 API、以及配合 netstat/openssl 等系统工具做发信链路诊断。验证环境:
java -version 2>&1 | head -1
mvn -v | head -1
实测回显:
openjdk version "17.0.20.1" 2026-08-18
Apache Maven 3.6.3
准备发行包与依赖 jar(手动安装方式,依赖 jar 缺一不可,原因见第二章):
sudo mkdir -p /opt/mail
cd /opt/mail
sudo curl -fsSLO https://archive.apache.org/dist/commons/email/binaries/commons-email-1.6.0-bin.tar.gz
sudo tar xzf commons-email-1.6.0-bin.tar.gz
cd commons-email-1.6.0
sudo mkdir -p lib
cd lib
sudo curl -fsSLO https://repo1.maven.org/maven2/com/sun/mail/jakarta.mail/1.6.7/jakarta.mail-1.6.7.jar
sudo curl -fsSLO https://repo1.maven.org/maven2/com/sun/activation/jakarta.activation/1.2.1/jakarta.activation-1.2.1.jar
该命令无输出(下载与解压成功即静默完成)。准备一个用于实测的发信目标:本机的 SMTP 服务器监听 25 端口,另有一台 465 端口的加密服务器。
💡 技术拓展:命令行发信不止 Java 一种方式。下表对比常见形态,本文的主角是 Commons Email:
| 方式 | 命令/接口 | 是否需要 Java | 特点 |
|---|---|---|---|
| Commons Email | java -cp 运行 Java 程序 | 是 | 面向程序内嵌发信,API 丰富 |
| mail/mailx | mail -s 主题 收件人 | 否 | 系统自带 CLI,交互式或脚本化 |
| sendmail | /usr/sbin/sendmail -t | 否 | MTA 原生投递接口 |
| curl | curl --mail-from ... smtp://host | 否 | 命令行直接走 SMTP,适合脚本 |
Commons Email 本身没有命令行入口,必须由程序驱动;后三种是脱离 Java 的运维侧发信手段,适合 shell 脚本。
二、发行包的命令行形态与 API 查看
先回答一个问题:commons-email-1.6.0.jar 能不能当命令行工具直接运行?答案藏在 jar 清单里。查看清单:
cd /opt/mail/commons-email-1.6.0
mkdir -p mf && cd mf
jar xf ../commons-email-1.6.0.jar META-INF/MANIFEST.MF
grep -E "Implementation-Version|Build-Jdk-Spec|Automatic-Module" META-INF/MANIFEST.MF
实测回显:
Build-Jdk-Spec: 21
Implementation-Version: 1.6.0
Automatic-Module-Name: org.apache.commons.mail
清单里没有 Main-Class,所以 java -jar 必然失败:
cd /opt/mail
java -jar commons-email-1.6.0/commons-email-1.6.0.jar
echo "退出码: $?"
实测回显:
no main manifest attribute, in commons-email-1.6.0/commons-email-1.6.0.jar
退出码: 1
Automatic-Module-Name: org.apache.commons.mail 说明它是模块化友好的纯库;没有入口类,就无法「一行命令发信」。命令行查看类 API 用 javap。先看最简的 SimpleEmail:
cd /opt/mail
javap -cp commons-email-1.6.0/commons-email-1.6.0.jar org.apache.commons.mail.SimpleEmail
实测回显:
Compiled from "SimpleEmail.java"
public class org.apache.commons.mail.SimpleEmail extends org.apache.commons.mail.Email {
public org.apache.commons.mail.SimpleEmail();
public org.apache.commons.mail.Email setMsg(java.lang.String) throws org.apache.commons.mail.EmailException;
}
SimpleEmail 只比基类多一个 setMsg,其余方法继承自 Email。家族类里各子类的继承关系与关键方法(javap 实测汇总):
| 类 | 继承自 | 关键方法 | 用途 |
|---|---|---|---|
| 抽象基类 | setHostName/setSmtpPort/setFrom/addTo/send | 连接与头部参数 | |
| SimpleEmail | setMsg(String) | 纯文本邮件 | |
| MultiPartEmail | addPart(...)、attach(EmailAttachment) | 正文+附件 | |
| HtmlEmail | MultiPartEmail | setHtmlMsg/setTextMsg/embed(File...) | HTML+内联图 |
| ImageHtmlEmail | HtmlEmail | setDataSourceResolver(...) | 模板自动内联本地图 |
| EmailAttachment | 独立类 | setPath/setName/setDisposition | 附件描述 |
MultiPartEmail 的 javap 实测(节选):
public class org.apache.commons.mail.MultiPartEmail extends org.apache.commons.mail.Email {
public org.apache.commons.mail.MultiPartEmail attach(org.apache.commons.mail.EmailAttachment) throws org.apache.commons.mail.EmailException;
public org.apache.commons.mail.Email addPart(java.lang.String, java.lang.String) throws org.apache.commons.mail.EmailException;
HtmlEmail 的 javap 实测(节选):
public class org.apache.commons.mail.HtmlEmail extends org.apache.commons.mail.MultiPartEmail {
public java.lang.String embed(java.io.File) throws org.apache.commons.mail.EmailException;
public java.lang.String embed(java.io.File, java.lang.String) throws org.apache.commons.mail.EmailException;
public void setHtmlMsg(java.lang.String) throws org.apache.commons.mail.EmailException;
EmailAttachment 的 javap 实测(节选):
public class org.apache.commons.mail.EmailAttachment {
public static final java.lang.String ATTACHMENT;
public static final java.lang.String INLINE;
public java.lang.String getPath();
public java.lang.String getName();
ATTACHMENT 与 INLINE 两个常量即附件处置方式(attachment 普通附件、inline 内联资源)。
三、家族类逐类实操
有了 jar 与依赖,逐个跑通家族类的典型用法。先是 SimpleEmail 纯文本:
cd /opt/mail
cat > HelloEmail.java <<'EOF'
import org.apache.commons.mail.Email;
import org.apache.commons.mail.SimpleEmail;
public class HelloEmail {
public static void main(String[] args) throws Exception {
Email 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 CLI demo");
email.setMsg("hello commons email, from CLI demo");
email.addTo("receiver@quzuo.net");
System.out.println("邮件已发送, Message-ID: " + email.send());
}
}
EOF
javac -encoding UTF-8 -cp "commons-email-1.6.0/commons-email-1.6.0.jar:commons-email-1.6.0/lib/*" HelloEmail.java
java -Dfile.encoding=UTF-8 -cp ".:commons-email-1.6.0/commons-email-1.6.0.jar:commons-email-1.6.0/lib/*" HelloEmail
实测回显:
邮件已发送, Message-ID: <1254526270.0.1791909232371@localhost>
MultiPartEmail 带附件(先把附件文件与图片放好):
cd /opt/mail
printf "Commons Email 附件测试文件\n" > lab.txt
cat > MultiDemo.java <<'EOF'
import org.apache.commons.mail.EmailAttachment;
import org.apache.commons.mail.MultiPartEmail;
public class MultiDemo {
public static void main(String[] args) throws Exception {
EmailAttachment attachment = new EmailAttachment();
attachment.setPath("/opt/mail/lab.txt");
attachment.setDisposition(EmailAttachment.ATTACHMENT);
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);
System.out.println("MultiPartEmail 发送成功, Message-ID: " + email.send());
}
}
EOF
javac -encoding UTF-8 -cp "commons-email-1.6.0/commons-email-1.6.0.jar:commons-email-1.6.0/lib/*" MultiDemo.java
java -Dfile.encoding=UTF-8 -cp ".:commons-email-1.6.0/commons-email-1.6.0.jar:commons-email-1.6.0/lib/*" MultiDemo
实测回显:
MultiPartEmail 发送成功, Message-ID: <1551380888.1.1791909662850@localhost>
HtmlEmail 内联图片——注意 embed(String, String) 的第一参是 URL 而非本地路径,本地文件要用 embed(File)/embed(File, cid):
cd /opt/mail
cat > HtmlDemo.java <<'EOF'
import java.io.File;
import org.apache.commons.mail.HtmlEmail;
public class HtmlDemo {
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/logo.png"), "quzuo-logo");
email.setHtmlMsg("<html><body><h2>Hello Commons Email</h2>"
+ "<img src=\"cid:" + cid + "\"/><p>内联图片已嵌入。</p></body></html>");
System.out.println("HtmlEmail 发送成功, cid: " + cid + ", Message-ID: " + email.send());
}
}
EOF
javac -encoding UTF-8 -cp "commons-email-1.6.0/commons-email-1.6.0.jar:commons-email-1.6.0/lib/*" HtmlDemo.java
java -Dfile.encoding=UTF-8 -cp ".:commons-email-1.6.0/commons-email-1.6.0.jar:commons-email-1.6.0/lib/*" HtmlDemo
实测回显:
HtmlEmail 发送成功, cid: quzuo-logo, Message-ID: <441975467.3.1791909709019@localhost>
logo.png 由一条命令生成的最小 PNG:
python3 -c "
import base64
png=base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAXklEQVR4nO3QMQEAAAgDoJnc6hgF00gB7nRAbwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4NfAFAAABm8cZ4gAAAABJRU5ErkJggg==')
open('/opt/mail/logo.png','wb').write(png)
"
实测回显:
(无输出,即写入成功)
embed() 返回的 cid 即内容标识符,HTML 里用 <img src="cid:值"> 引用。
ImageHtmlEmail 比 HtmlEmail 更进一步:模板里的本地图片路径会被自动解析并内联,需要给它一个 DataSourceResolver:
cd /opt/mail
cat > ImageDemo.java <<'EOF'
import java.io.File;
import org.apache.commons.mail.ImageHtmlEmail;
import org.apache.commons.mail.resolver.DataSourceFileResolver;
public class ImageDemo {
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")));
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");
System.out.println("ImageHtmlEmail 发送成功, Message-ID: " + email.send());
}
}
EOF
javac -encoding UTF-8 -cp "commons-email-1.6.0/commons-email-1.6.0.jar:commons-email-1.6.0/lib/*" ImageDemo.java
java -Dfile.encoding=UTF-8 -cp ".:commons-email-1.6.0/commons-email-1.6.0.jar:commons-email-1.6.0/lib/*" ImageDemo
实测回显:
ImageHtmlEmail 发送成功, Message-ID: <222641023.2.1791909777277@localhost>
DataSourceFileResolver 的构造参数是解析基准目录,HTML 里的 src="logo.png" 相对该目录被解析为本地文件。
EmailAttachment 的四种处置方式(源码常量实测):
| 处置常量 | 报文位置 | 客户端表现 |
|---|---|---|
| EmailAttachment.ATTACHMENT(即 "attachment") | Content-Disposition: attachment | 显示为可下载附件 |
| EmailAttachment.INLINE(即 "inline") | Content-Disposition: inline | 随正文内联显示 |
实测一条完整 SMTP 会话(SimpleEmail 发信时服务器端抓到的报文,节选):
EHLO localhost
MAIL FROM:<sender@quzuo.net>
RCPT TO:<receiver@quzuo.net>
DATA
Date: Tue, 13 Oct 2026 16:31:13 +0000 (GMT)
From: sender@quzuo.net
To: receiver@quzuo.net
Subject: Commons Email CLI demo
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
hello commons email, from CLI demo
从会话可读出:EHLO 打招呼 → MAIL FROM 发件人 → RCPT TO 收件人 → DATA 后跟报文头与正文。正文头部 Content-Type: text/plain; charset=UTF-8 即 setCharset("UTF-8") 的效果。
3.1 jshell 交互实操
jshell 适合把库当「命令行」来探索。把语句存成脚本(jshell 脚本模式不会自动回显表达式值,必须显式 System.out.println):
cat > /opt/mail/hello-email.jsh <<'EOF'
import org.apache.commons.mail.*;
SimpleEmail email = new SimpleEmail();
email.setHostName("127.0.0.1");
email.setSmtpPort(25);
email.setCharset("UTF-8");
email.setFrom("sender@quzuo.net");
email.setSubject("jshell probe");
email.setMsg("hello from jshell");
email.addTo("receiver@quzuo.net");
System.out.println("发送结果: " + email.send());
/exit
EOF
cd /opt/mail
export LANG=C.UTF-8
jshell --class-path "commons-email-1.6.0/commons-email-1.6.0.jar:commons-email-1.6.0/lib/*" hello-email.jsh
实测回显:
发送结果: <687241927.0.1791911099833@localhost>
三个实测确认的 jshell 使用要点:脚本文件模式不回显表达式值,必须显式 System.out.println(这里把 send() 的返回值打出来);/exit 结束会话;终端未设 UTF-8 时中文会显示成问号,先 export LANG=C.UTF-8(或给 jshell 加 -J-Dfile.encoding=UTF-8)即可正常。
3.2 javac + java -cp 手动运行要点
类路径写法要点:Linux 分隔符是冒号 :(Windows 是分号);. 让 JVM 找到当前目录的 HelloEmail.class;lib/* 由 JVM 展开,不要让 shell 先于 JVM 展开。缺依赖时直接报错,这也是依赖 jar 必须齐备的原因:
java -Dfile.encoding=UTF-8 -cp "commons-email-1.6.0/commons-email-1.6.0.jar" HelloEmail.java
实测回显(节选,实测首尾):
Exception in thread "main" java.lang.NoClassDefFoundError: javax/mail/MessagingException
at HelloEmail.main(HelloEmail.java:6)
Caused by: java.lang.ClassNotFoundException: javax.mail.MessagingException
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
四、发信端口的命令行诊断
发信之前用系统工具确认端口可达,能省掉大量「连接失败」的排查。先看本机监听的发信端口:
netstat -lnt | grep -E ":25|:465|:587"
实测回显:
tcp 0 0 0.0.0.0:25 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:465 0.0.0.0:* LISTEN
两行分别是明文 SMTP(25)与加密 SMTPS(465)在监听;若某个端口没有这一行,就说明该端口没有服务,发信必然连接失败。(netstat -lntp 加 -p 还可显示进程,需要 root;ss -lnt 是 netstat 的现代替代。)
再对加密端口做一次 TLS 握手,确认证书与加密套件:
echo "" | openssl s_client -connect 127.0.0.1:465
实测回显(节选,实测首尾):
depth=0 CN = mail.quzuo.net, O = Quzuo, C = CN
verify error:num=18:self-signed certificate
CONNECTED(00000003)
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIDTTCCAjWgAwIBAgIUC4BB6//hEt/GOeppNgALz559tLAwDQYJKoZIhvcNAQEL
BQAwNjEXMBUGA1UEAwwObWFpbC5xdXp1by5uZXQxDjAMBgNVBAoMBVF1enVvMQsw
(实测首尾示例,中间为证书 DER 的 base64 数据,此处省略中段)
No client certificate CA names sent
--- New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Verify return code: 18 (self-signed certificate)
可读出三点:TLS 握手成功(CONNECTED);协商出的协议是 TLSv1.3、套件是 TLS_AES_256_GCM_SHA384;Verify return code: 18 (self-signed certificate) 表示服务器用的自签证书,客户端未信任——这正是 Java 侧需要 truststore 的原因(见《Apache Commons Email 配置参数详解》加密章节)。
对明文端口用 -starttls smtp 探测是否支持 STARTTLS:
echo "" | openssl s_client -connect 127.0.0.1:25 -starttls smtp
实测回显(节选,实测首尾):
Didn't find STARTTLS in server response, trying anyway...
0A00010B:SSL routines:ssl3_get_record:wrong version number:../ssl/record/ssl3_record.c:354:
CONNECTED(00000003)
---
no peer certificate available
---
Verify return code: 0 (ok)
Didn't find STARTTLS in server response 说明该服务器未在 EHLO 里广告 STARTTLS,明文继续就会握手失败(wrong version number)——这也解释了为什么在 Commons Email 里对这类服务器开 setStartTLSRequired(true) 会报 STARTTLS is required but host does not support STARTTLS。
发信端口诊断速查:
| 命令 | 检查内容 | 关键判读 |
|---|---|---|
netstat -lnt | grep :25 | 明文 SMTP 是否监听 | 有 LISTEN 行即可达 |
netstat -lnt | grep :465 | SMTPS 是否监听 | 有 LISTEN 行即可达 |
openssl s_client -connect host:465 | TLS 握手与证书 | CONNECTED + 读 Cipher 行 |
openssl s_client -connect host:25 -starttls smtp | 是否支持 STARTTLS | Didn't find STARTTLS 即不支持 |
五、常见问题
Q1:java -jar commons-email-1.6.0.jar 报 no main manifest attribute?
Commons Email 是纯库,清单里没有 Main-Class(见第二章实测清单),不能当命令行工具运行。要运行自己的代码,用 java -cp "主jar:lib/*" 你的主类。
Q2:HtmlEmail.embed("路径", "名字") 报 Invalid URL: no protocol?
embed(String, String) 的第一参被当作 URL。本地文件要用 embed(File) 或 embed(File, cid),例如 email.embed(new File("/opt/mail/logo.png"), "quzuo-logo")。
Q3:jshell 脚本跑完没有输出?
脚本文件模式不回显表达式值,必须显式 System.out.println;/exit 结束会话。交互式会话才会自动回显。
Q4:终端里程序输出的中文变问号?
locale 问题。export LANG=C.UTF-8,或给 java/jshell 加 -Dfile.encoding=UTF-8。
Q5:netstat 看不到端口对应的进程?
netstat -lntp 的 -p 需要 root 权限才能显示进程名与 PID;也可以换用 ss -lntp。只判断端口是否监听用不带 -p 的 netstat -lnt 即可。
六、参考链接
- Apache Commons Email 官方主页:https://commons.apache.org/proper/commons-email/
- 1.6.0 官方 javadoc(各家族类 API):https://commons.apache.org/proper/commons-email/apidocs/index.html
- 官方用户指南(Simple/MultiPart/Html 用法):https://commons.apache.org/proper/commons-email/userguide.html
- Maven 中央仓库 1.6.0(pom 与 jar):https://repo1.maven.org/maven2/org/apache/commons/commons-email/1.6.0/
- jshell 用户指南:https://docs.oracle.com/en/java/javase/17/jshell/
- OpenSSL s_client 手册:https://www.openssl.org/docs/man1.1.1/man1/openssl-s_client.html
- netstat/ss 手册:https://man7.org/linux/man-pages/man8/ss.8.html