Apache Commons Email 常用 CLI 命令与工具详解

实测 Apache Commons Email 1.6.0 命令行用法:jar 无 Main-Class、java -cp 启动、javap 查看家族类 API、家族类逐个实操,并用 netstat、openssl 诊断发信端口。

Apache Commons EmailApache Commons Email2026/10/14

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 Emailjava -cp 运行 Java 程序是面向程序内嵌发信,API 丰富
mail/mailxmail -s 主题 收件人否系统自带 CLI,交互式或脚本化
sendmail/usr/sbin/sendmail -t否MTA 原生投递接口
curlcurl --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 实测汇总):

类继承自关键方法用途
Email抽象基类setHostName/setSmtpPort/setFrom/addTo/send连接与头部参数
SimpleEmailEmailsetMsg(String)纯文本邮件
MultiPartEmailEmailaddPart(...)、attach(EmailAttachment)正文+附件
HtmlEmailMultiPartEmailsetHtmlMsg/setTextMsg/embed(File...)HTML+内联图
ImageHtmlEmailHtmlEmailsetDataSourceResolver(...)模板自动内联本地图
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 :465SMTPS 是否监听有 LISTEN 行即可达
openssl s_client -connect host:465TLS 握手与证书CONNECTED + 读 Cipher 行
openssl s_client -connect host:25 -starttls smtp是否支持 STARTTLSDidn'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 即可。

六、参考链接

  1. Apache Commons Email 官方主页:https://commons.apache.org/proper/commons-email/
  2. 1.6.0 官方 javadoc(各家族类 API):https://commons.apache.org/proper/commons-email/apidocs/index.html
  3. 官方用户指南(Simple/MultiPart/Html 用法):https://commons.apache.org/proper/commons-email/userguide.html
  4. Maven 中央仓库 1.6.0(pom 与 jar):https://repo1.maven.org/maven2/org/apache/commons/commons-email/1.6.0/
  5. jshell 用户指南:https://docs.oracle.com/en/java/javase/17/jshell/
  6. OpenSSL s_client 手册:https://www.openssl.org/docs/man1.1.1/man1/openssl-s_client.html
  7. netstat/ss 手册:https://man7.org/linux/man-pages/man8/ss.8.html