Postorius 1.3.13 API 使用详解

Postorius 1.3.13 体系自动化接口全解:Mailman core REST API(域/列表/成员/Held Message 审核端点实测回显)、mailmanclient Python 实战姿势要点、mailman-web 管理命令清单,附 Basic 认证、分页、-d 表单编码等实测结论与 8 条真实 FAQ。

Postorius2026/10/26

Postorius 1.3.13 API 使用详解

目标:讲透 Postorius 1.3.13 体系的自动化接口。Postorius 本身是纯 Web 应用(无自带 CLI、也不暴露自有 API),它的「API 语境」由三部分组成:Mailman core 的 REST API(Postorius 页面背后调用的同一套接口,是列表自动化的主通道)、mailmanclient Python 库(官方 SDK,Postorius 内部也在用它)、mailman-web 的 Django 管理命令(运维侧批量操作)。本文逐组实测演示,全部命令与回显采集于 2026-10-26 的 Compose 部署环境(部署见 《Postorius 1.3.13 Docker Compose 部署指南》)。

一、环境信息

  • 栈:postgres:16-alpine + maxking/mailman-core:rolling + maxking/mailman-web:rolling(版本事实见部署指南第一章——core REST 实测返回 GNU Mailman 3.3.11b1、镜像内 falcon 4.4.0)
  • 访问端点(宿主机视角):
    • core REST:http://127.0.0.1:8156/(容器 8001)
    • Web:http://127.0.0.1:8155/(容器 8000)
    • 容器互联视角:http://mailman-core:8001(web 容器内访问 core)
  • 凭据:restadmin / restpass(core 侧 [webservice] 与 web 侧 MAILMAN_REST_* 成对,见配置详解篇)
  • 测试数据起点:空库(0 域 0 列表),本文按序创建并在文末清理

⚠️ 语法前置结论(两套环境实测一致):mailman REST 的写操作参数只认 -d 表单编码(application/x-www-form-urlencoded)与查询串;-F(multipart/form-data)参数一律不被解析,稳定复现 400 Missing Parameter(core 3.3.10+falcon 3.1.3 与 3.3.11b1+falcon 4.4.0 双双实测)。本文所有 POST/PATCH 均用 -d。

二、API 总览

2.1 认证与入口

core REST 使用 HTTP Basic 认证(-u restadmin:restpass);无凭据或口令错误一律 401(实测回显):

curl -s -o /dev/null -w 'wrong password: %{http_code}\n' -u restadmin:wrongpass http://127.0.0.1:8156/3.0/lists
wrong password: 401

入口自描述与版本信息(实测回显):

curl -s -u restadmin:restpass http://127.0.0.1:8156/3.0/system/versions
{"mailman_version": "GNU Mailman 3.3.11b1 (Tom Sawyer)", "python_version": "3.12.15 (main, Oct  1 2026, 13:38:06) [GCC 15.2.0]", "api_version": "3.0", "self_link": "http://mailman-core:8001/3.0/system/versions", "http_etag": "\"163e423af0427a156744135e2e44721bb301ce08\""}

api_version: 3.0;core 同时提供 /3.1/(增量改进,资源结构兼容)。生产脚本建议固定 /3.0/ 或 /3.1/ 显式版本段,不要裸 /3.0 之外的隐式根。

2.2 核心资源地图

资源端点方法用途(Postorius 对应页面)
邮件域/3.0/domainsGET/POST/DELETEDomains 管理页
列表集合/3.0/listsGET/POST列表索引页
单列表/3.0/lists/{list_id}GET/DELETE列表页
列表配置/3.0/lists/{list_id}/configGET/PATCH列表 Settings 各标签页
成员(全局订阅)/3.0/membersPOSTMass Subscription 页
成员花名册/3.0/lists/{list_id}/roster/{role}GETSubscription 页
单成员/3.0/members/{member_id}GET/PATCH/DELETE订阅者行内操作
待审请求/3.0/lists/{list_id}/heldGETHeld Message 页
待审动作/3.0/lists/{list_id}/held/{request_id}POSTAccept/Reject/Discard 按钮
用户/地址/3.0/users、/3.0/addressesGET/POST账号相关

要点:list_id 是把 @ 换成 . 的列表标识(apitest@lists.example.com → apitest.lists.example.com),路径里永远用 list_id;成员资源用 40 位数字的 member_id 定位。

2.3 分页

集合端点统一支持 count + page,返回 total_size / next_page / previous_page(实测回显——共 1 个列表、取每页 1 条):

curl -s -u restadmin:restpass "http://127.0.0.1:8156/3.0/lists?count=1&page=1" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print('total:', d['total_size'], '| shown:', len(d.get('entries',[])), '| next page:', d.get('next_page'))"
total: 1 | shown: 1 | next page: None

三、域与列表管理 API

3.1 建域、建列、改配置(完整生命周期)

# ① 建邮件域(列表必须挂在已存在的域下,顺序不能反)
curl -s -u restadmin:restpass -X POST http://127.0.0.1:8156/3.0/domains \
  -d mail_host=lists.example.com -w "HTTP %{http_code}\n"

# ② 建列表
curl -s -u restadmin:restpass -X POST http://127.0.0.1:8156/3.0/lists \
  -d fqdn_listname=apitest@lists.example.com -w "HTTP %{http_code}\n"

# ③ 读列表信息
curl -s -u restadmin:restpass http://127.0.0.1:8156/3.0/lists/apitest.lists.example.com \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print({k:d[k] for k in ('fqdn_listname','list_id','display_name','volume')})"

# ④ 改列表配置(PATCH,端点是 config 不是 settings)
curl -s -u restadmin:restpass -X PATCH http://127.0.0.1:8156/3.0/lists/apitest.lists.example.com/config \
  -d display_name="API Test List" -d description="created via REST api" -w "HTTP %{http_code}\n"

# ⑤ 读回配置核对
curl -s -u restadmin:restpass http://127.0.0.1:8156/3.0/lists/apitest.lists.example.com/config \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print({k:d[k] for k in ('display_name','description','default_nonmember_action','default_member_action')})"

实测回显(①–⑤):

HTTP 201
HTTP 201
{'fqdn_listname': 'apitest@lists.example.com', 'list_id': 'apitest.lists.example.com', 'display_name': 'Apitest', 'volume': 1}
HTTP 204
{'display_name': 'API Test List', 'description': 'created via REST api', 'default_nonmember_action': 'hold', 'default_member_action': 'defer'}
  • ①② 返回 201 Created,响应头 Location 指向新资源(脚本可从 Location 直接拿 self_link);
  • ④ 的 PATCH 返回 204 No Content——写操作成功即 204 无响应体;
  • config 端点可改的字段就是 Postorius Settings 页的全部表单项:description、info、subscription_policy、default_nonmember_action、archive_policy 等约 60 项,GET config 一次全量返回;
  • 域不存在就建列表会报 400 Domain does not exist(Postorius 界面同理,先建域)。

3.2 删除(204 清理)

curl -s -u restadmin:restpass -X DELETE http://127.0.0.1:8156/3.0/lists/apitest.lists.example.com -w "HTTP %{http_code}\n"
curl -s -u restadmin:restpass -X DELETE http://127.0.0.1:8156/3.0/domains/lists.example.com -w "HTTP %{http_code}\n"
HTTP 204
HTTP 204

DELETE 成功返回 204;再 GET 同资源得 404。删除列表不影响用户/地址本体(它们是独立资源)。

四、成员管理 API

4.1 订阅、花名册、单成员读改删

(按 3.1 先重建域与列表 apitest 后执行;订阅走全局 members 端点,用 list_id 指明目标列表)

# ① 订阅(三重 pre_* 免确认,适合脚本直加)
curl -s -u restadmin:restpass -X POST http://127.0.0.1:8156/3.0/members \
  -d list_id=apitest.lists.example.com -d subscriber=alice@lists.example.com \
  -d display_name=Alice -d pre_verified=True -d pre_confirmed=True \
  -d pre_approved=True -w "HTTP %{http_code}\n"

# ② 读花名册(role=member)
curl -s -u restadmin:restpass \
  http://127.0.0.1:8156/3.0/lists/apitest.lists.example.com/roster/member \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print('count:', d['total_size']); e=d['entries'][0]; print('member_id:', e['member_id'], 'role:', e['role'], 'address:', e['address'])"

# ③ 读单成员(member_id 从 ② 获得)
curl -s -u restadmin:restpass http://127.0.0.1:8156/3.0/members/219544869497648381031774027548267832258

# ④ 改成员审核动作(hold = 该成员来信全部进待审)
curl -s -u restadmin:restpass -X PATCH http://127.0.0.1:8156/3.0/members/219544869497648381031774027548267832258 \
  -d moderation_action=hold -w "HTTP %{http_code}\n"

# ⑤ 退订
curl -s -u restadmin:restpass -X DELETE http://127.0.0.1:8156/3.0/members/219544869497648381031774027548267832258 -w "HTTP %{http_code}\n"

实测回显(①–④,⑤ 见行内):

HTTP 201
count: 1
member_id: 219544869497648381031774027548267832258 role: member address: http://mailman-core:8001/3.0/addresses/alice@lists.example.com
HTTP 204

③ 的关键字段(实测 JSON 节选):

"email": "alice@lists.example.com",
"list_id": "apitest.lists.example.com",
"role": "member",
"delivery_status": "enabled",
"delivery_mode": "regular",
"moderation_action": "hold",
"member_id": 219544869497648381031774027548267832258
  • role 取值 member/owner/moderator——花名册端点按角色分查(roster/owner、roster/moderator);
  • PATCH 可改 moderation_action(defer/hold/reject/discard/accept)、delivery_status(enabled/by_moderator/by_bounces)、display_name 等;
  • 新成员未 PATCH 过审核动作时,GET member 可能没有 moderation_action 键(缺省=随列表默认策略,实测行为),PATCH 一次即固化显式值;
  • ⑤ DELETE 返回 204,退订完成。

五、Held Message 审核 API

5.1 制造一条待审请求

测试环境没有外接 postfix,用 core 自带的 LMTP 端口(容器内 8024)直接投递模拟「成员来信」——这正是生产中 postfix 的投递方式。注意 LMTP 绑定在容器主机名(MM_HOSTNAME=mailman-core)而非 127.0.0.1(实测 127.0.0.1 连接被拒):

docker exec postorius-1-3-13-core python3 -c "
import smtplib
from email.message import EmailMessage
m = EmailMessage()
m['From']='alice@lists.example.com'
m['To']='apitest@lists.example.com'
m['Subject']='held by member moderation'
m['Message-ID']='<held-0001@lists.example.com>'
m.set_content('This message should be held for moderation.')
l=smtplib.LMTP('mailman-core', 8024)
l.sendmail('alice@lists.example.com',['apitest@lists.example.com'], m.as_bytes())
l.quit()
print('LMTP injected')
"
LMTP injected

4.1 步骤 ④ 已把 alice 的 moderation_action 设为 hold,几秒后查询待审队列(实测回显):

curl -s -u restadmin:restpass http://127.0.0.1:8156/3.0/lists/apitest.lists.example.com/held \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print('total:', d['total_size']); [print('request_id:', e['request_id'], '| subject:', e['subject'], '| reason:', e['reason']) for e in d.get('entries',[])]"
total: 1
request_id: 1 | subject: held by member moderation | reason: The message comes from a moderated member

5.2 审核动作:accept / discard / reject

# 放行(进入外发队列)
curl -s -u restadmin:restpass -X POST http://127.0.0.1:8156/3.0/lists/apitest.lists.example.com/held/1 \
  -d action=accept -w "HTTP %{http_code}\n"
# 丢弃(再注入一条后执行)
curl -s -u restadmin:restpass -X POST http://127.0.0.1:8156/3.0/lists/apitest.lists.example.com/held/2 \
  -d action=discard -w "HTTP %{http_code}\n"
# 拒收(同时向发件人发退信通知,参数 action=reject)

实测回显:

HTTP 204
HTTP 204

动作执行后 GET held 的 total_size 即刻归零(实测两次动作后均为 0)。动作全集:accept(放行)、reject(拒绝并通知发件人)、discard(静默丢弃)、defer(暂缓回队列)——与 Postorius Held Message 页四个按钮一一对应。

⚠️ 两个实测坑:① 对 held 资源用 DELETE 会得到 405 Method Not Allowed——审核动作必须 POST action=(实测踩中);② DELETE /3.0/requests/{request_id} 在本版本实测 404(请求 ID 存在仍 404),清理待审一律走 POST 动作端点最稳。

六、mailmanclient Python 实战

mailmanclient 是官方 REST SDK(Postorius 内部同款)。在 mailman-web 镜像内直接可用(裸机环境在 venv 里 pip install mailmanclient,B1 环境为 3.3.5)。实测脚本(容器内运行,2>&1 去除无关告警行):

# docker exec postorius-1-3-13-web python - <<'EOF'
from mailmanclient import Client
c = Client('http://mailman-core:8001/3.0', 'restadmin', 'restpass')
dom = c.get_domain('lists.example.com')
print('domain:', dom.mail_host)
ml = dom.create_list('pydemo')                    # 注意:参数是裸列表名,非完整地址
print('list created:', ml.fqdn_listname)
member = ml.subscribe('bob@lists.example.com', 'Bob',
                      pre_verified=True, pre_confirmed=True, pre_approved=True)
print('subscribed:', member.email, '| role:', member.role)
ml = c.get_list('pydemo.lists.example.com')
print('roster count:', sum(1 for _ in ml.members))
print('display_name:', ml.settings['display_name'])
print('held messages:', sum(1 for _ in ml.held))
c.delete_list('pydemo.lists.example.com')
dom.delete()
print('cleanup: list and domain deleted')
# EOF

实测回显:

domain: lists.example.com
list created: pydemo@lists.example.com
subscribed: bob@lists.example.com | role: member
roster count: 1
display_name: Pydemo
held messages: 0
cleanup: list and domain deleted

实测踩出的三个姿势要点:

  1. Domain.create_list(list_name) 收裸列表名('pydemo'),传完整地址会拼出 pydemo@lists.example.com@lists.example.com 报 400 Domain does not exist(实测复现);
  2. ml.subscribe() 直接返回 Member 对象(不是 response,没有 status_code 属性,实测 AttributeError);
  3. c.create_list 不存在——建列表只有 Client.create_domain() + Domain.create_list() 两条腿;Client.delete_list(fqdn_listname) 收完整地址,两者不对称(源码实测核对)。

常用只读组合:c.get_lists() 全量列表、ml.settings 配置字典(支持 ml.settings['description']='x'; ml.settings.save() 写回)、ml.held 待审队列、c.find_lists('apitest') 按名搜索。

七、mailman-web 管理命令

7.1 入口形态

环境入口示例
裸机(pip 装 mailman-web 包)venv 里的 mailman-web 包装命令su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman-web migrate'
Compose/镜像工程目录 manage.pydocker exec postorius-1-3-13-web python /opt/mailman-web/manage.py <命令>

两者都等价于 Django 管理命令(自动装载 MAILMAN_WEB_CONFIG/settings)。部署时已实战过的四件套:migrate(建表)、collectstatic(静态资源)、compress(SCSS 压缩)、createsuperuser(管理员)。

7.2 命令清单(实测 manage.py help 节选,按应用分组)

[postorius]
    mmclient            # 交互式 REST 客户端 shell(试用 API 用)
    reset_passwords     # 批量重置用户密码

[hyperkitty]
    mailman_sync        # 与 core 双向同步列表元数据
    update_index_one_list / update_index   # Whoosh 全文索引更新
    hyperkitty_import   # mbox 历史归档导入

[django_q]
    qcluster / qinfo / qmonitor            # 异步任务集群与监控

[haystack]
    rebuild_index / clear_index / update_index

[sessions]
    clearsessions       # 清理过期会话(建议 cron 日跑)

💡 [postorius] mmclient 是官方内置的 REST 试玩场:进入后可直接敲 c = client; [l.fqdn_listname for l in c.lists] 这类表达式验证 API 语义,再翻译成 curl/Python。配合第六章的 mailmanclient 使用效率最高。

八、常见问题(FAQ)

Q1:curl 返回 401? Basic 凭据与 core 侧 [webservice] 的 admin_user/admin_pass 不一致。注意这是 core 的凭据(不是 Postorius 登录账号),修改需按配置详解篇生效矩阵两端成对改。

Q2:POST 参数传了却报 400 Missing Parameter? 两种情况:用了 -F(multipart)——本 API 只认 -d 表单编码(第一章实测结论);或参数名拼错(本 API 是大小写敏感的 snake_case,如 fqdn_listname、mail_host)。

Q3:建列表报 400 Domain does not exist? 先建域再建列(顺序硬约束)。脚本里按 3.1 的 ①→② 固定顺序,或封装成「域不存在则创建」的幂等函数。

Q4:用 mailmanclient Domain.create_list 报 Domain does not exist: xxx@xxx? 参数传成了完整地址。该方法的形参是裸列表名(第六章姿势要点 1):dom.create_list('pydemo') 而不是 dom.create_list('pydemo@lists.example.com')。

Q5:accept 放行的邮件没有送达订阅者? accept 只把邮件从待审队列移入外发队列,真正的 SMTP 投递由 core 的 [mta] 配置决定(Compose 栈默认 SMTP_HOST 指向 core 自身,无外发中继时投递会重试失败)。生产需在 core 侧配好 SMTP 中继(部署指南 4.2 的 SMTP_HOST 变量)。审核 API 语义本身不受影响。

Q6:待审请求能 DELETE 吗? 不能:DELETE /3.0/lists/{list_id}/held/{id} 实测 405;DELETE /3.0/requests/{id} 实测 404(即使 ID 有效)。清理/审核一律 POST …/held/{id} -d action=accept|reject|discard|defer(第五章实测)。

Q7:列表很多时怎么高效翻页/搜索? 集合端点用 ?count=50&page=2 分页(2.3 实测),next_page 字段给了下一页 self_link 可直接跟进;mailmanclient 侧用 c.get_list_page(count=50, page=1) 与 c.find_lists('关键词')。

Q8:从 Postorius 页面看到的动作,怎么定位到具体 API? 浏览器开发者工具的 Network 面板直接看 Postorius 发出的 REST 请求(方法+路径+表单参数)——Postorius 就是这套 API 的参考实现;本文第二、三、四、五章的资源地图即按该链路整理并逐一实测。

九、参考链接

(实测环境:Compose 栈 maxking/mailman-core:rolling + mailman-web:rolling,全部命令与回显采集日期 2026-10-26;core REST 实测版本 3.3.11b1,与 PyPI 稳定线 3.3.10 的差异见部署指南镜像选型章)