Apache Pony Mail 0.11-incubating 常用 CLI 命令与工具详解

Apache Pony Mail 0.11-incubating 常用 CLI 命令与工具详解:初始化/归档/导入/列表管理/诊断工具与 curl 验证 web 与 ES 的用法及真实回显,命令均基于实测

Apache Pony MailApache Pony Mail2026/10/13

Apache Pony Mail 0.11-incubating 常用 CLI 命令与工具详解

目标:详解 Apache Pony Mail 0.11-incubating 随包附带的命令行工具——setup.py(初始化)、archiver.py(归档入口)、import-mbox.py(历史导入)、list-lists.py / edit-list.py / copy-list.py(列表管理)、trace.py / elastic.py(调试诊断),以及用 curl 验证 web/ES 链路的常用命令。所有命令与回显均为真实环境实测。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • Apache Pony Mail:0.11-incubating(工具目录 /opt/ponymail/tools)
  • Elasticsearch:2.4.6(http://127.0.0.1:9200,索引 ponymail)
  • python-elasticsearch:2.4.0
  • 运行方式:全部工具在 /opt/ponymail/tools/ 目录下以 python3 <工具>.py 执行
  • 示例账号:root(安装管理)、esuser(ES 进程账号,密码 q1w2e3r4)

二、工具概览

工具位置用途
setup.py/opt/ponymail/tools/初始化:连接 ES、建索引、生成 ponymail.cfg 与 config.lua
archiver.py/opt/ponymail/tools/归档入口:从 STDIN 读原始邮件写入 ES
import-mbox.py/opt/ponymail/tools/批量导入 mbox / maildir / pipermail / mod_mbox 历史数据
list-lists.py/opt/ponymail/tools/列出归档中的全部列表
edit-list.py/opt/ponymail/tools/列表批量编辑:改名、公私切换、删除、正文混淆
copy-list.py/opt/ponymail/tools/列表复制/迁移(可跨索引)
trace.py/opt/ponymail/tools/调试追踪输出
elastic.py/opt/ponymail/tools/ES 连通性与版本诊断
curl系统自带验证 web API 与 ES 数据

三、前置条件

  • 已完成 Pony Mail 0.11-incubating 安装:ES 2.4.6 运行中(systemctl is-active elasticsearch 为 active)、ponymail.cfg 已由 setup.py 生成、python-elasticsearch 2.x 库已安装(⚠️ 1.x 库会使 archiver.py 拒绝启动)。
  • 所有工具依赖 ponymail.cfg,需在 /opt/ponymail/tools/ 目录下执行。

四、工具详解

4.1 setup.py —— 初始化工具

作用:连接 ES、探测引擎与库版本、创建索引(含 mbox/attachment/mbox_source/mailinglists/account/notifications 六类映射)、生成 ponymail.cfg 与 site/api/lib/config.lua。

帮助回显:

usage: setup.py [-h] [--defaults] [--dbprefix DBPREFIX] [--clobber]
                [--dbhost DBHOST] [--dbport DBPORT] [--dbname DBNAME]
                [--dbshards DBSHARDS] [--dbreplicas DBREPLICAS]
                [--mailserver MAILSERVER] [--mldom MLDOM] [--wordcloud]
                [--skiponexist] [--noindex] [--nocloud]
                [--generator GENERATOR]

options:
  -h, --help            show this help message and exit
  --defaults            Use default settings
  --dbprefix DBPREFIX
  --clobber             Allow overwrite of ponymail.cfg &
                        ../site/api/lib/config.lua (default: create *.tmp if
                        either exists)
  --dbhost DBHOST       ES backend hostname
  --dbport DBPORT       DB port
  --dbname DBNAME       ES DB name
  --dbshards DBSHARDS   DB Shard Count
  --dbreplicas DBREPLICAS
                        DB Replica Count
  --mailserver MAILSERVER
                        Host name of outgoing mail server
  --mldom MLDOM         Domains to accept mail for via UI
  --wordcloud           Enable word cloud
  --skiponexist         Skip setup if ES index exists
  --noindex             Don't make an ES index, assume it exists
  --nocloud             Do not enable word cloud
  --generator GENERATOR
                        Document ID Generator to use (legacy, medium, cluster,
                        full)

常用形式:

cd /opt/ponymail/tools
python3 setup.py --defaults --clobber          # 用默认值并允许覆盖已存在配置
python3 setup.py --dbhost 10.0.0.5 --dbport 9200 --dbname mail \
                 --dbshards 3 --dbreplicas 1 --generator cluster

实测回显(--defaults):

Welcome to the Pony Mail setup script!
Let's start by determining some settings...

Okay, I got all I need, setting up Pony Mail...
Versions: library 2 (2.4.0), engine 2 (2.4.6)
Creating index ponymail
Index created! {'acknowledged': True}
Writing importer config (ponymail.cfg)
mod_lua configuration (config.lua)
Copying sample JS config to config.js (if needed)...
All done, Pony Mail should...work now :)

--defaults 默认值:ES localhost:9200、索引名 ponymail、分片 1、副本 0、mailserver localhost、可回复域 example.org、wordcloud 开、生成器 cluster。索引已存在且未加 --skiponexist 时脚本报错退出(防误覆盖)。

4.2 archiver.py —— 归档入口

作用:从 STDIN 读取一封原始邮件,按 List-ID 归类、解析正文/附件,写入 ES。生产环境由 MTA 别名管道或 Mailman3 插件调用;也可手工喂 stdin 验证。

帮助回显:

usage: archiver.py [-h] [--lid LID] [--altheader ALTHEADER]
                   [--allowfrom ALLOWFROM] [--ignore IGNOREFROM] [--private]
                   [--makedate] [--quiet] [--verbose] [--html2text] [--dry]
                   [--dumponfail DUMP]

options:
  -h, --help            show this help message and exit
  --lid LID             Alternate specific list ID
  --altheader ALTHEADER
                        Alternate header for list ID
  --allowfrom ALLOWFROM
                        (optional) source IP (mail server) to allow posts
                        from, ignore if no match
  --ignore IGNOREFROM   Sender/list to ignore input from (owner etc)
  --private             This is a private archive
  --makedate            Use the archive timestamp as the email date instead of
                        the Date header
  --quiet               Do not exit -1 if the email could not be parsed
  --verbose             Output additional log messages
  --html2text           Try to convert HTML to text if no text/plain message
                        is found
  --dry                 Do not save emails to elasticsearch, only test parsing
  --dumponfail DUMP     If pushing to ElasticSearch fails, dump documents in
                        JSON format to this directory and fail silently.

STDIN 手动归档(实测,邮件含 List-ID: <ponymail-test.example.org>,经 lmtpd 投递):

[lmtpd] rc=0 Mon, 12 Oct 2026 21:33:23 -0000: Done archiving to <ponymail-test.example.org> as re040e3adc798ee1db640fe1c080917dffb7995d7f75f54b7577fe1bb@<ponymail-test.example.org>!

落库核对:

curl -s 'http://127.0.0.1:9200/ponymail/mbox/_count'

回显:

{"count":2,"_shards":{"total":1,"successful":1,"failed":0}}

💡 无 List-ID 的邮件输出 Nothing to import (no list-id found!) 且不落库;用 --altheader delivered-to 指定替代头,或用 --lid '<list.name.example.org>' 强制指定。--dry 只测解析不写库,适合上线前排错。

4.3 import-mbox.py —— 历史数据导入

作用:从本地 mbox/maildir 文件、mod_mbox 站点或 pipermail 页面批量导入历史邮件,支持 --dedup 按 Message-ID 去重、--lid 统一列表 ID。

帮助回显(节选):

usage: import-mbox.py [-h] [--source SOURCE] [--dir] [--interactive] [--quick]
                      [--mod-mbox] [--pipermail] [--lid LISTID]
                      [--project PROJECT] [--ext EXT] [--domain DOMAIN]
                      [--private] [--dry] [--duplicates] [--html2text]
                      [--requirelid] [--dedup] [--ignorebody IBODY]
                      [--resend RESEND] [--timeout TIMEOUT]
                      [--filter FROMFILTER] [--nomboxo]

options:
  -h, --help           show this help message and exit
  --source SOURCE      Source to scan (http(s)://, imap(s):// or file path)
  --dir                Input is in Maildir format
  --quick              Only grab the first file you can find
  --mod-mbox           This is mod_mbox, derive list-id and files from it
  --pipermail          This is pipermail, derive files from it (list ID has to
                       be set!)
  --lid LISTID         Optional List-ID to override source with. Format:
                       <list-name>@<domain>
  --project PROJECT    Optional project to look for ($project-* will be
                       imported as well)
  --ext EXT            Optional file extension e.g. ".gz" (or call it with an
                       empty string to not care)
  --domain DOMAIN      Optional domain extension for MIDs and List ID
                       reconstruction)
  --private            This is a privately archived list. Filter through auth
                       proxy.
  --dedup              Try to dedup messages based on ID before importing
  --requirelid         Require a List ID to be present, ignore otherwise
  --timeout TIMEOUT    Optional timeout in secs for importing an mbox/maildir
                       file (default is 600 seconds)
  --filter FROMFILTER  Optional sender filter: Only import emails from this
                       address

本地 mbox 导入实测:

cd /opt/ponymail/tools
python3 import-mbox.py --source /tmp/mboxtest/test.mbox --lid '<ponymail-test.example.org>' --dedup

回显:

Thread-1: 1 elements left to slurp
Thread-1: Slurping /tmp/mboxtest/test.mbox
Thread-1: Parsed 1 records (failed: 0) from /tmp/mboxtest/test.mbox
Thread-1: Done, 0 elements left to slurp
All done! 1 records inserted/updated after 0 seconds. 0 records were bad and ignored

💡 导入与实时归档使用同一 digest 口径(ponymail.cfg 的 generator),同 Message-ID 重复导入不产生重复文档;--requirelid 可跳过无 List-ID 的邮件。

4.4 list-lists.py —— 列表清单

帮助回显:

usage: list-lists.py [-h] [--dbname DBNAME] [--pretty] [--debug] [--counts]

options:
  -h, --help       show this help message and exit
  --dbname DBNAME  Override index name
  --pretty         Convert List IDs to email addresses
  --debug          Output the result JSON instead, very noisy!
  --counts         Show the count of messages for each list

实测(含已导入的两封邮件):

cd /opt/ponymail/tools
python3 list-lists.py --pretty

回显:

ponymail-test@example.org

--counts 追加每列表邮件数;--debug 输出原始 JSON。

4.5 edit-list.py —— 列表批量编辑

作用:批量改名、改描述、公私切换、删除、正文混淆(可选 --test 试运行不落库)。

帮助回显:

usage: edit-list.py [-h] [--source SOURCE | --mid MID] [--rename TARGET]
                    [--desc DESC] [--obfuscate OBFUSCATE]
                    [--private | --public] [--delete] [--wildcard] [--debug]
                    [--notag] [--test]

options:
  -h, --help            show this help message and exit
  --source SOURCE       Source list to edit
  --mid MID             Source Message-ID to edit
  --rename TARGET       (optional) new list ID
  --desc DESC           (optional) new list description
  --obfuscate OBFUSCATE
                        Things to obfuscate in body, if any
  --private             Make all emails in list private
  --public              Make all emails in list public
  --delete              Delete emails from this list
  --wildcard            Allow wildcards in --source
  --debug               Debug output - very noisy!
  --notag               List IDs do not have <> in them
  --test                Only test for occurrences, do not run the chosen
                        action (dry run)

试运行实测(--public --test,不产生实际变更):

cd /opt/ponymail/tools
python3 edit-list.py --source '<ponymail-test.example.org>' --public --test

回显:

Beginning list edit:
  - List ID: <ponymail-test.example.org>
  - Action: Mark all emails public
DRY RUN - NO CHANGES WILL BE MADE
Updating docs...
All done, processed 2 docs in 0 seconds

💡 只给 --source 不给动作会输出 Nothing to do! No target list ID or action specified(实测)。生产执行前务必先加 --test 核对影响文档数。

4.6 copy-list.py —— 列表复制/迁移

帮助回显:

usage: copy-list.py [-h] --source <list id> [--target <list id>]
                    [--newdb <index name>] [--wildcard] [--notag]

options:
  -h, --help            show this help message and exit
  --source <list id>    Source list to edit
  --target <list id>    (optional) new list ID
  --newdb <index name>  (optional) new ES database name
  --wildcard            Allow wildcards in --source
  --notag               List IDs do not have <> in them

示例(把列表复制成新 ID 或迁到新索引):

cd /opt/ponymail/tools
python3 copy-list.py --source '<ponymail-test.example.org>' --target '<ponymail-test-archive.example.org>'

4.7 trace.py 与 elastic.py —— 调试诊断

trace.py 是代码级追踪工具,直接运行输出测试追踪行(实测):

>>>>[1]trace.py@62#<module>: test
>>>>[0]trace.py@45#trace: test
>>>>[1]trace.py@64#<module>: test

elastic.py 打印库/引擎版本并测试 ES 连通与索引存在性(实测):

Versions: Library: 2 (2, 4, 0) Engine: 2 (2.4.6)
True
False
  • 第一行:库版本 2.4.0、引擎版本 2.4.6(两者一致说明归档器可运行);
  • 第二行 True:ES 连接成功;
  • 第三行 False:默认索引校验结果(按脚本内置逻辑输出)。

4.8 curl —— web 与 ES 验证命令

# 验证 ES 引擎
curl -s http://127.0.0.1:9200/
# 验证索引内邮件数
curl -s 'http://127.0.0.1:9200/ponymail/mbox/_count'
# 验证 web 前端与 Lua API
curl -s http://127.0.0.1:8080/api/pminfo.lua
# 验证某封邮件可通过 web 检索
curl -s 'http://127.0.0.1:8080/api/email.lua?id=re040e3adc798ee1db640fe1c080917dffb7995d7f75f54b7577fe1bb%40%3Cponymail-test.example.org%3E'
# 查看线程视图
curl -s 'http://127.0.0.1:8080/api/thread.lua?id=re040e3adc798ee1db640fe1c080917dffb7995d7f75f54b7577fe1bb%40%3Cponymail-test.example.org%3E'

实测回显:

# /api/pminfo.lua
{"no_active_lists":0,"no_threads":0,"activity":{},"participants":0,"hits":0}

# /api/email.lua?id=... (节选)
{"tid":"re040e3adc...@<ponymail-test.example.org>","references":"","private":false,
 "message-id":"<lmtp-test-001@example.org>",...,"subject":"Pony Mail LMTP test",
 "body":"Hello from the LMTP import verification!...","attachments":{}}

# /api/thread.lua?id=...
{"emails":[{"tid":"re040e...","subject":"Pony Mail LMTP test",...}]}

五、注意事项

  • ⚠️ 所有工具在 /opt/ponymail/tools/ 目录下执行(依赖同目录 ponymail.cfg)。
  • ⚠️ python-elasticsearch 必须 2.x(实测 1.x 库报 Exception: ('Unexpected elasticsearch version ', ...))。
  • 归档/导入以 ponymail.cfg 的 generator 为去重口径,变更生成器会造成历史重复。
  • edit-list.py / copy-list.py 属批量变更工具,先 --test 试运行核对影响范围再执行。
  • 每条命令逐个单独执行;批量处理多个列表时逐条列出,勿用循环脚本。

六、常见问题

6.1 import-mbox.py 导入后 count 不增加

解决:同 Message-ID 被 digest 去重(正常现象,实测重复导入 All done! 1 records inserted/updated 但 count 不变);确认 generator 未变更。

6.2 archiver.py 报 Nothing to import (no list-id found!)

解决:邮件模板补 List-ID 头,或加 --altheader delivered-to / --lid '<list.name.example.org>' 指定列表。

6.3 工具报 Unexpected elasticsearch version

解决:pip3 install 'elasticsearch==2.4.0' 锁定 2.x 库后重试。

6.4 setup.py 报索引已存在

解决:索引已建(防误覆盖),加 --skiponexist 静默跳过,或改 --dbname 用新索引。

6.5 elastic.py 输出 False(连接失败)

解决:先 systemctl is-active elasticsearch 确认 ES 运行;核对 ponymail.cfg 的 hostname/port 与实际一致。

七、参考链接