Ubuntu 22.04 安装 Postorius 1.3.13 指南
Ubuntu 22.04 从零安装 Postorius 1.3.13(Mailman 3 列表管理界面)全流程:core+web 双组件、PostgreSQL 双库、uwsgi+nginx+systemd 托管、postfix LMTP 回流,含在线 venv/pip 与离线 wheel 两方案、falcon 限版等实测坑、端到端登录与归档链路验证回显与 8 条真实排障 FAQ。
Ubuntu 22.04 安装 Postorius 1.3.13 指南
目标:在一台 Ubuntu 22.04 服务器上从零安装 GNU Mailman 3 的列表管理 Web 界面 Postorius 1.3.13——Postorius 从不单独安装,它随 mailman-web 工程包与 Mailman core 引擎一起交付,因此本文实际交付的是「core + Postorius(含 HyperKitty 归档前端)」完整双组件栈:PostgreSQL 双数据库、uwsgi + nginx 承载、systemd 托管、postfix 投递回流,以及「登录 Postorius → 列表可见 → 发信 → 归档可查」的端到端验证,全部命令与回显均在真实环境实测通过。Postorius 的定位与选型背景见 《Postorius 1.3.13 介绍与选型指南》。
一、环境信息
本篇实测环境为单台 Ubuntu 22.04 服务器,主机名 pm1.example.com,邮件域 example.com:
- 操作系统:Ubuntu 22.04.5 LTS,x86_64
- 应用版本:Postorius 1.3.13、Mailman core 3.3.10、HyperKitty 1.3.12(同栈随装)、django-mailman3 1.3.15、mailman-web 0.0.9、mailmanclient 3.3.5、Django 4.2.30、django-haystack 3.4.0、Whoosh 2.7.4、psycopg2-binary 2.9.13、alembic 1.14.1、SQLAlchemy 1.4.54、falcon 3.1.3(限版原因见 4.4 节与 FAQ 8.1)
- 系统组件:PostgreSQL 14.24、nginx 1.18.0(Ubuntu 官方仓库版)、uwsgi 2.0.20-debian(含 uwsgi-plugin-python3)、postfix 3.6.4、Python 3.10.12(系统自带)+ 独立 venv、pip 26.2.2
- 端口规划(本机监听):
| 端口 | 服务 | 绑定 | 用途 |
|---|---|---|---|
| 25 | postfix | 0.0.0.0 | 收取列表邮件,LMTP 转投 core |
| 80 | nginx | 0.0.0.0 | 对外 Web 入口(Postorius/HyperKitty) |
| 8000 | uwsgi(http-socket) | 127.0.0.1 | Web 应用(归档推送 API 走这里) |
| 8001 | mailman core REST | 127.0.0.1 | Mailman 管理 API(Postorius 读列表数据) |
| 5432 | PostgreSQL | 127.0.0.1 | 双数据库:core 用 mailman,Web 用 mailmanweb |
- 目录规划:
| 路径 | 用途 |
|---|---|
| /opt/mailman/venv | Python 3.10 虚拟环境(core 与 web 共用) |
| /etc/mailman3/ | 三份核心配置:mailman.cfg、mailman-hyperkitty.cfg、settings.py |
| /opt/mailman/web/ | mailman-web 包固定的工作目录(BASE_DIR 写死,日志等落在其 logs/ 子目录) |
| /opt/mailman/mm/var/ | core 运行时目录(队列/日志/锁/归档 spool) |
| /opt/mailman/mm/static | Django collectstatic 产物(nginx 直接托管) |
| /opt/mailman/mm/fulltext_index | Whoosh 全文索引目录 |
| /etc/uwsgi/mailman-web.ini | uwsgi 应用配置 |
⚠️ 网络说明:本篇全部软件来自 Ubuntu 22.04 官方 apt 仓库与 PyPI 官方源(pypi.org),2026-10-26 实测均可直连下载;若你的服务器访问 PyPI 缓慢,改走第五章离线安装方案。
二、前置条件
- 一台 Ubuntu 22.04 服务器,建议 2 核 CPU / 2GB 内存 / 10GB 磁盘起步;
- root 或具备 sudo 权限的账户;
- DNS 已就绪:
pm1.example.com的 A 记录指向本机(浏览器访问用),example.com域的 MX 记录指向本机(列表收信用)——没有 MX 也能完成安装与本地验证,只是收不到外部来信; - 服务器时间已同步(
timedatectl确认); - 80/25 端口未被占用:
ss -ltnp | grep -E ':80 |:25 '应无其他进程监听。
三、版本选型说明
为什么是 Postorius 1.3.13:PyPI 上 Postorius 最新稳定版即 1.3.13(2024-09-21 发布,2026-10-26 查询),要求 Python ≥ 3.9、django ≥4.2,<5.1——Ubuntu 22.04 自带的 Python 3.10 满足要求。版本溯源与生态背景见 《Postorius 1.3.13 介绍与选型指南》。
配套组件版本矩阵(本次实测组合,亲测可用):
| 组件 | 版本 | 说明 |
|---|---|---|
| mailman | 3.3.10 | core 引擎,REST 端口 8001 |
| postorius | 1.3.13 | 本篇主角,列表管理 Web 界面 |
| hyperkitty | 1.3.12 | 归档 Web 应用,随 mailman-web 同栈安装 |
| django-mailman3 | 1.3.15 | 套件公共库(账号/主题/列表元数据) |
| mailman-web | 0.0.9 | Django 工程模板包,提供 mailman-web 管理命令与 settings 基座 |
| mailmanclient | 3.3.5 | Postorius/HyperKitty 访问 core REST 的客户端库 |
| django-haystack | 3.4.0 | 搜索抽象层 |
| Whoosh | 2.7.4 | 纯 Python 全文搜索引擎(零外部服务依赖) |
| psycopg2-binary | 2.9.13 | PostgreSQL 驱动 |
| alembic / SQLAlchemy | 1.14.1 / 1.4.54 | ⚠️ 必须限制在 1.14.x / 1.4.x,见下方依赖链说明 |
| falcon | 3.1.3 | ⚠️ 必须限制 <4,见下方依赖链说明 |
⚠️ 依赖链两个关键坑(均为实测踩中):
- mailman 3.3.10 对 SQLAlchemy 2.x 与 alembic 1.15+ 不兼容(core 初始化直接报错退出),pip 安装必须带
"alembic<1.15" "sqlalchemy<2.0";- mailman 3.3.10 的 REST API 依赖 falcon 3 的表单参数解析机制(
wsgiapp.py显式开启auto_parse_form_urlencoded,falcon 4 已移除该选项),装上默认解析出的 falcon 4.4 后,REST 建列表等所有表单 POST 一律报400 Missing Parameter(实测复现,详见 FAQ 8.1),必须带"falcon<4"限版。两处限版已写入第四章的安装命令,从源头规避。
四、在线安装方案(A:官方 venv + pip,推荐)
4.1 方案说明
这是 GNU Mailman 3 官方文档推荐的标准路径:系统 apt 装 Postgres/nginx/uwsgi/postfix 与编译依赖 → venv 里 pip 装全套 Python 组件 → 写三份配置(core 的 mailman.cfg、归档插件的 mailman-hyperkitty.cfg、Web 的 settings.py)→ Django 初始化(建表/静态资源/压缩/管理员)→ uwsgi + nginx + systemd 托管 → postfix 把列表邮件 LMTP 回流给 core → 端到端验证。全程约 30–40 分钟。
4.2 安装系统依赖
export DEBIAN_FRONTEND=noninteractive
echo "postfix postfix/mailname string example.com" | debconf-set-selections
echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections
apt update
apt install -y python3-venv python3-dev libpq-dev gcc curl \
postgresql nginx uwsgi uwsgi-plugin-python3 sassc postfix
安装要点:预置 postfix 的 debconf 应答避免交互向导(邮件域 example.com、站点类型 Internet Site);python3-venv 提供 venv 模块;libpq-dev 与 gcc 供 psycopg2 编译兜底;curl 用于后面的 REST API 验证(Ubuntu 22.04 最小安装不带它,实测踩过);sassc 是 Django Compressor 编译 SCSS 的系统依赖,缺它 compress 步骤报错(见 FAQ 8.7)。
验证(实测回显):
dpkg-query -W postgresql nginx postfix uwsgi uwsgi-plugin-python3 python3.10-venv
nginx 1.18.0-6ubuntu14.21
postfix 3.6.4-1ubuntu1.4
postgresql 14+238
python3.10-venv 3.10.12-1~22.04.18
uwsgi 2.0.20-4
uwsgi-plugin-python3 2.0.20-4
4.3 安装并初始化 PostgreSQL(双库)
Web 侧与 core 各用一个独立数据库(官方设计:订阅者数据与归档数据分离)。先启动数据库,再建用户与两个库:
systemctl start postgresql
su - postgres -c "psql -c \"CREATE USER mailman WITH PASSWORD 'q1w2e3r4';\""
su - postgres -c "psql -c 'CREATE DATABASE mailman OWNER mailman;'"
su - postgres -c "psql -c 'CREATE DATABASE mailmanweb OWNER mailman;'"
实测回显依次为 active(is-active 检查)、CREATE ROLE、CREATE DATABASE、CREATE DATABASE。验证双库就位:
su - postgres -c 'psql -tAc "SELECT datname FROM pg_database ORDER BY 1"' | grep mailman
mailman
mailmanweb
4.4 创建系统用户、虚拟环境与工作目录
core 与 uwsgi 都以专用系统用户 mailman 运行(不建 home、不可登录):
useradd -r -M -d /opt/mailman -s /usr/sbin/nologin mailman
mkdir -p /opt/mailman/mm /opt/mailman/web/logs
python3 -m venv /opt/mailman/venv
/opt/mailman/venv/bin/pip install --upgrade pip
⚠️
/opt/mailman/web/logs必须在跑第一条mailman-web命令前建好:mailman-web 0.0.9 的 BASE_DIR 固定写死为/opt/mailman/web,其日志 handler 启动即打开/opt/mailman/web/logs/mailmanweb.log,目录缺失时所有管理命令直接崩(实测报ValueError: Unable to configure handler 'file',详见 FAQ 8.3)。
pip 安装全套组件(一条命令,注意三处限版):
/opt/mailman/venv/bin/pip install \
"mailman==3.3.10" "mailman-hyperkitty==1.2.1" \
"postorius==1.3.13" "django-mailman3==1.3.15" \
"mailman-web==0.0.9" "django-haystack==3.4.0" "whoosh==2.7.4" \
"psycopg2-binary==2.9.13" "alembic<1.15" "sqlalchemy<2.0" "falcon<4"
输出节选(实测 Successfully installed 行,可按需缩短):
Successfully installed Mako-1.4.3 MarkupSafe-3.0.4 Pygments-2.21.0 aiosmtpd-1.4.6 alembic-1.14.1 asgiref-3.12.1 ... django-4.2.30 django-allauth-65.19.7 ... django-haystack-3.4.0 django-mailman3-1.3.15 ... falcon-3.1.3 ... hyperkitty-1.3.12 ... mailman-3.3.10 mailman-hyperkitty-1.2.1 mailman-web-0.0.9 mailmanclient-3.3.5 ... postorius-1.3.13 psycopg2-binary-2.9.13 ... sqlalchemy-1.4.54 ... whoosh-2.7.4 ...
验证(实测回显):
/opt/mailman/venv/bin/pip show postorius django-mailman3 django-haystack whoosh \
| grep -E '^(Name|Version)'
Name: postorius
Version: 1.3.13
Name: django-mailman3
Version: 1.3.15
Name: django-haystack
Version: 3.4.0
Name: Whoosh
Version: 2.7.4
/opt/mailman/venv/bin/pip show mailman mailman-hyperkitty psycopg2-binary alembic SQLAlchemy falcon \
| grep -E '^(Name|Version)'
Name: mailman
Version: 3.3.10
Name: mailman-hyperkitty
Version: 1.2.1
Name: psycopg2-binary
Version: 2.9.13
Name: alembic
Version: 1.14.1
Name: SQLAlchemy
Version: 1.4.54
Name: falcon
Version: 3.1.3
4.5 编写三份核心配置
创建配置目录并把属主交给 mailman:
mkdir -p /etc/mailman3 /opt/mailman/mm
chown -R mailman:mailman /opt/mailman /etc/mailman3
① core 主配置 /etc/mailman3/mailman.cfg:
[mailman]
layout: here
site_owner: admin@example.com
default_language: en
[paths.here]
var_dir: /opt/mailman/mm/var
log_dir: /opt/mailman/mm/var/log
queue_dir: /opt/mailman/mm/var/queue
ext_dir: /opt/mailman/mm/var/ext
template_dir: /opt/mailman/mm/var/templates
spool_dir: /opt/mailman/mm/var/spool
pid_file: /opt/mailman/mm/var/master.pid
lock_file: /opt/mailman/mm/var/master.lck
data_dir: /opt/mailman/mm/var/data
cache_dir: /opt/mailman/mm/var/cache
[database]
class: mailman.database.postgresql.PostgreSQLDatabase
url: postgresql+psycopg2://mailman:q1w2e3r4@localhost:5432/mailman
[archiver.hyperkitty]
class: mailman_hyperkitty.Archiver
enable: yes
configuration: /etc/mailman3/mailman-hyperkitty.cfg
[webservice]
host: 127.0.0.1
port: 8001
admin_user: restadmin
admin_pass: restpass
逐项说明:
layout: here+[paths.here]:全部运行时产物收敛到/opt/mailman/mm/var,备份与卸载只处理这一棵目录树;[database]:core 用mailman库。url 必须写postgresql+psycopg2://前缀,写裸postgresql://时 core 无法识别方言直接报错(FAQ 8.2);[archiver.hyperkitty]:启用归档插件,邮件过信后由 core HTTP 推送给 HyperKitty;[webservice]:core 的 REST API 监听与凭据,Postorius 就是通过这组凭据读列表数据的,只绑 127.0.0.1 不对外。
② 归档插件配置 /etc/mailman3/mailman-hyperkitty.cfg:
[general]
base_url: http://localhost:8000/hyperkitty/
api_key: SecretArchiverAPIKey
base_url:插件把邮件 POST 给 HyperKitty 的归档 API 入口,指向本机 uwsgi 的 http-socket(8000)——不是 nginx 的 80;api_key:双向共享的令牌,必须与 ③ settings.py 里的MAILMAN_ARCHIVER_KEY完全一致,否则推送被 403 拒绝。生产环境请换长随机串。
③ Web 侧 Django 配置 /etc/mailman3/settings.py(mailman-web 包自带完整工程基座,我们的文件只做覆盖层):
from mailman_web.settings.base import *
from mailman_web.settings.mailman import *
SECRET_KEY = "pm3-prod-secret-key-change-me-0123456789abcdef"
DEBUG = False
ALLOWED_HOSTS = ["pm1.example.com", "localhost", "127.0.0.1"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mailmanweb",
"USER": "mailman",
"PASSWORD": "q1w2e3r4",
"HOST": "localhost",
"PORT": "5432",
}
}
TIME_ZONE = "Asia/Shanghai"
USE_TZ = True
STATIC_ROOT = "/opt/mailman/mm/static"
MEDIA_ROOT = "/opt/mailman/mm/media"
STATIC_URL = "/static/"
MEDIA_URL = "/media/"
MAILMAN_REST_API_URL = "http://localhost:8001"
MAILMAN_REST_API_USER = "restadmin"
MAILMAN_REST_API_PASS = "restpass"
MAILMAN_ARCHIVER_KEY = "SecretArchiverAPIKey"
MAILMAN_ARCHIVER_FROM = ("127.0.0.1", "::1")
POSTORIUS_TEMPLATE_BASE_URL = "http://localhost:8000"
DEFAULT_FROM_EMAIL = "postorius@example.com"
SERVER_EMAIL = "postorius@example.com"
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "http"
HAYSTACK_CONNECTIONS = {
"default": {
"ENGINE": "haystack.backends.whoosh_backend.WhooshEngine",
"PATH": "/opt/mailman/mm/fulltext_index",
}
}
逐项说明:
- 前两行
from mailman_web.settings...是关键:base 提供全部 Django/第三方应用默认项,mailman 提供与 core 的连接约定,我们的文件只写覆盖项; MAILMAN_REST_API_URL/USER/PASS:Postorius 连 core 的 REST 地址与凭据,与 mailman.cfg[webservice]成对修改——这是 Postorius 能不能看到列表数据的总开关;MAILMAN_ARCHIVER_KEY:与 ② 的api_key一致,HyperKitty 校验归档推送请求的Authorization: Token头;POSTORIUS_TEMPLATE_BASE_URL:Postorius 渲染列表页模板时的回源基址,指向本机 uwsgi 的 HTTP 端口;EMAIL_BACKEND:演示用 console 后端(系统邮件打到日志);生产接真实 SMTP 时换成django.core.mail.backends.smtp.EmailBackend并补 EMAIL_HOST 等参数,否则用户收不到订阅确认邮件。
settings 是如何被加载的:mailman-web 0.0.9 的 wsgi 与管理命令入口都会先执行 mailman_web.manage.setup()——它读取环境变量 MAILMAN_WEB_CONFIG(缺省 /etc/mailman3/settings.py),把该文件所在目录加入 sys.path 并将 DJANGO_SETTINGS_MODULE 设为文件名(settings)。因此 uwsgi 与 systemd 单元里都要带上这个环境变量(见 4.7/4.9)。全部参数的逐项深解见后续配置详解篇。
4.6 初始化 Django(建表/静态资源/压缩/管理员)
mailman-web 包提供统一管理命令入口 mailman-web(等价于 Django 的 manage.py)。依次执行建表、静态资源收集、SCSS 压缩:
chown -R mailman:mailman /opt/mailman
su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman-web migrate'
实测节选(首尾;开头两行 allauth 兼容性提示不影响安装,属 mailman-web 0.0.9 预置设置与新 allauth 版本的已知告警):
System check identified some issues:
WARNINGS:
?: settings.ACCOUNT_AUTHENTICATION_METHOD is deprecated, use: settings.ACCOUNT_LOGIN_METHODS = {'email', 'username'}
?: settings.ACCOUNT_EMAIL_REQUIRED is deprecated, use: settings.ACCOUNT_SIGNUP_FIELDS = ['email*', 'username*', 'password1*', 'password2*']
Operations to perform:
...
Applying socialaccount.0005_socialtoken_nullable_app... OK
Applying socialaccount.0006_alter_socialaccount_extra_data... OK
(首次建表会逐条 Applying … OK 走完 account/admin/auth/django_mailman3/django_q/hyperkitty/postorius/sessions/sites/socialaccount 全部迁移。)
su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman-web collectstatic --noinput'
387 static files copied to '/opt/mailman/mm/static'.
su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman-web compress --force'
Compressing... done
Compressed 2 block(s) from 80 template(s) for 1 context(s).
创建管理员账号并设密码(演示约定:admin / q1w2e3r4):
su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman-web createsuperuser --noinput --username admin --email admin@example.com'
su -s /bin/bash mailman -c 'echo "from django.contrib.auth.models import User; u=User.objects.get(username=\"admin\"); u.set_password(\"q1w2e3r4\"); u.save(); print(\"password set for\", u.username, \"is_superuser=\", u.is_superuser)" | /opt/mailman/venv/bin/mailman-web shell'
Superuser created successfully.
password set for admin is_superuser= True
(createsuperuser --noinput 建出无密码超管,用 Django shell 以 set_password 写入口令——相比交互式 changepassword,该方式可脚本化且便于核验。)
4.7 uwsgi 承载 Web 应用
为 Postorius/HyperKitty 单独写应用配置 /etc/uwsgi/mailman-web.ini:
[uwsgi]
plugins = python3
socket = /opt/mailman/mm/uwsgi.sock
chmod-socket = 666
http-socket = 127.0.0.1:8000
chdir = /opt/mailman/web
virtualenv = /opt/mailman/venv
module = mailman_web.wsgi:application
env = MAILMAN_WEB_CONFIG=/etc/mailman3/settings.py
master = true
processes = 4
threads = 2
harakiri = 60
max-requests = 500
vacuum = true
uid = mailman
gid = mailman
logto = /opt/mailman/mm/var/log/uwsgi.log
逐项说明:
socket与http-socket同时保留是本方案的核心要点:socket=开的是 uwsgi 二进制协议(nginx 的uwsgi_pass只认这个),http-socket=开的是原生 HTTP(core 的归档插件用 HTTP POST 推邮件、Postorius 模板渲染回源,只认这个)。只配前者,归档推送报RemoteDisconnected;只配后者,nginx 全站 502——两个坑都真实存在,详见 FAQ 8.4/8.5;virtualenv+module:直接用包内工程mailman_web.wsgi,无需自建 Django 项目目录;env = MAILMAN_WEB_CONFIG=...:4.5 节讲过的 settings 装载开关,漏写则应用落到内置默认配置、数据库连不上;harakiri/max-requests:单请求 60 秒自杀、每 500 请求回收 worker,防大附件渲染与内存泄漏拖垮服务。
4.8 nginx 反向代理
写入站点配置 /etc/nginx/sites-available/mailman-web 并启用:
server {
listen 80;
server_name pm1.example.com;
access_log /var/log/nginx/mailman-web.access.log;
error_log /var/log/nginx/mailman-web.error.log;
client_max_body_size 20m;
location /static/ {
alias /opt/mailman/mm/static/;
expires 30d;
}
location /media/ {
alias /opt/mailman/mm/media/;
expires 7d;
}
location / {
include uwsgi_params;
uwsgi_pass unix:/opt/mailman/mm/uwsgi.sock;
uwsgi_read_timeout 300;
}
}
ln -sf /etc/nginx/sites-available/mailman-web /etc/nginx/sites-enabled/mailman-web
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
实测回显(nginx -t):
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
- 动态请求走 unix socket(uwsgi 协议),静态/媒体由 nginx 直接 alias 接管,动静分离;
client_max_body_size 20m放开大附件上传场景。
URL 挂载地图(mailman-web 0.0.9 的根路由实测行为,Postorius 视角必看):
| 路径 | 实测行为 | 说明 |
|---|---|---|
/ | 301 → /mailman3/lists/ | 根路径永久重定向到列表索引 |
/mailman3/lists/ | 200 | Postorius 列表索引(mailman3 是 Postorius 的官方别名前缀) |
/postorius/lists/ | 200 | Postorius 原生前缀,同一套视图 |
/accounts/login/ | 200 | django-allauth 登录页 |
/hyperkitty/ | 200 | HyperKitty 归档前端(/archives/ 为其别名) |
4.9 systemd 托管双服务
/etc/systemd/system/mailman3.service(core):
[Unit]
Description=GNU Mailman 3 Core
After=network.target postgresql.service
[Service]
Type=forking
User=mailman
Group=mailman
Environment=MAILMAN_CONFIG_FILE=/etc/mailman3/mailman.cfg
ExecStart=/opt/mailman/venv/bin/mailman start --force
ExecStop=/opt/mailman/venv/bin/mailman stop
Restart=on-failure
[Install]
WantedBy=multi-user.target
/etc/systemd/system/mailman-web.service(uwsgi):
[Unit]
Description=Mailman 3 Web (Postorius/HyperKitty)
After=network.target postgresql.service
[Service]
Type=simple
User=mailman
Group=mailman
ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/mailman-web.ini
Restart=on-failure
KillSignal=SIGQUIT
ExecStop=/usr/bin/uwsgi --stop /opt/mailman/mm/var/uwsgi.pid
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable mailman3 mailman-web
systemctl start mailman3 mailman-web
实测回显(enable 两行 symlink 创建):
Created symlink /etc/systemd/system/multi-user.target.wants/mailman3.service → /etc/systemd/system/mailman3.service.
Created symlink /etc/systemd/system/multi-user.target.wants/mailman-web.service → /etc/systemd/system/mailman-web.service.
验证(实测回显):
systemctl is-active mailman3 mailman-web nginx postgresql postfix
systemctl is-enabled mailman3 mailman-web nginx
active
active
active
active
active
enabled
enabled
enabled
systemctl status mailman3 --no-pager | head -4
● mailman3.service - GNU Mailman 3 Core
Loaded: loaded (/etc/systemd/system/mailman3.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2026-10-26 10:46:34 UTC; 4min 30s ago
Process: 6088 ExecStart=/opt/mailman/venv/bin/mailman start --force (code=exited, status=0/SUCCESS)
systemctl status mailman-web --no-pager | head -3
● mailman-web.service - Mailman 3 Web (Postorius/HyperKitty)
Loaded: loaded (/etc/systemd/system/mailman-web.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2026-10-26 10:41:17 UTC; 9min ago
4.10 postfix:把列表邮件回流给 core
core 启动后会在 data 目录生成两张 postfix 映射表(postfix_lmtp/postfix_domains),postfix 据此把发往列表地址的邮件经 LMTP 转给 core(端口 8024,core 自管)。先确认映射表已生成,再用 postconf 写入关键参数:
ls /opt/mailman/mm/var/data/ | grep postfix
postfix_domains
postfix_domains.db
postfix_lmtp
postfix_lmtp.db
postconf -e "transport_maps = hash:/opt/mailman/mm/var/data/postfix_lmtp"
postconf -e "local_recipient_maps = proxy:unix:passwd.byname \$alias_maps hash:/opt/mailman/mm/var/data/postfix_lmtp"
postconf -e "relay_domains = example.com"
postconf -e "mydestination = localhost"
postconf -e "maillog_file = /var/log/postfix.log"
postmap hash:/opt/mailman/mm/var/data/postfix_lmtp
systemctl restart postfix
transport_maps:testlist@example.com→lmtp:127.0.0.1:8024的映射来源,core 每次列表变更自动重写该文件,改后需postmap重建 hash;local_recipient_maps追加postfix_lmtp:让 postfix 收件检查阶段就认得列表地址,避免「User unknown」退信;relay_domains = example.com+mydestination = localhost:把邮件域交给列表链路,本机系统用户的信仍走 local;maillog_file:显式把 postfix 日志落到文件便于排障。
4.11 端到端验证(Postorius 视角)
第一步:用 core REST API 建邮件域与列表。注意顺序——域必须先于列表存在,否则建列表报 Domain does not exist(实测踩过,见 FAQ 8.6)。凭据即 4.5 节 [webservice]:
curl -s -u restadmin:restpass -X POST http://127.0.0.1:8001/3.0/domains \
-d mail_host=example.com -d owner=admin@example.com -w "HTTP %{http_code}\n"
HTTP 201
curl -s -u restadmin:restpass -X POST http://127.0.0.1:8001/3.0/lists \
-d fqdn_listname=testlist@example.com -w "HTTP %{http_code}\n"
HTTP 201
验证(实测回显):
curl -s -u restadmin:restpass http://127.0.0.1:8001/3.0/lists \
| python3 -c "import sys,json; d=json.load(sys.stdin); print([e['fqdn_listname'] for e in d['entries']])"
['testlist@example.com']
⚠️ 参数传递写法:实测 mailman 3.3.10 的 REST 只认
-d表单编码(falcon 3 的auto_parse_form_urlencoded机制)与查询串,-F(multipart/form-data)参数永远不会被解析(falcon 3 与 4 下实测均报400 Missing Parameter)。本文全部用-d。另见 FAQ 8.1。
第二步:加一个订阅成员(后续发信人用它,非成员来信默认策略可能被扣住)。订阅走全局 members 端点,用 list_id(@ 换成 .)指明列表:
curl -s -u restadmin:restpass -X POST http://127.0.0.1:8001/3.0/members \
-d list_id=testlist.example.com -d subscriber=tester@example.com \
-d display_name=Tester -d pre_verified=True -d pre_confirmed=True \
-d pre_approved=True -w "HTTP %{http_code}\n"
HTTP 201
验证(实测回显):
curl -s -u restadmin:restpass \
http://127.0.0.1:8001/3.0/lists/testlist.example.com/roster/member \
| python3 -c "import sys,json; d=json.load(sys.stdin); print('count:', d['total_size'], 'members:', [e['address'] for e in d.get('entries',[])])"
count: 1 members: ['http://localhost:8001/3.0/addresses/tester@example.com']
第三步:以成员身份给列表发一封信(走本机 postfix 的 sendmail 接口):
/usr/sbin/sendmail -f tester@example.com testlist@example.com <<'EOF'
From: Tester <tester@example.com>
To: testlist@example.com
Subject: [Testlist] Postorius web ui test message
Message-ID: <pm-test-0001@example.com>
Date: Sun, 26 Oct 2026 02:50:00 +0000
Hello, this is a Postorius stack end-to-end test message.
EOF
第四步:验证链路五个断面。
① core 日志确认收信与归档插件推送成功(实测回显):
grep 'HyperKitty archived message' /opt/mailman/mm/var/log/mailman.log | tail -1
Oct 26 10:49:37 2026 (6097) HyperKitty archived message <pm-test-0001@example.com> to https://example.com/archives/list/testlist@example.com/message/7XRGOTDNFHGWA2TGKVYEUD7SMRUDH3TC/
② 插件本地 spool 无积压(推送失败会落 …/archives/hyperkitty/spool/ 等重试,正常应为 0):
ls -A /opt/mailman/mm/var/archives/hyperkitty/spool/ | wc -l
0
③ HyperKitty 归档 API 用 Token 直查(插件同款调用方式,实测回显):
curl -s -o /dev/null -w '%{http_code}\n' \
'http://127.0.0.1:8000/hyperkitty/api/mailman/urls?mlist=testlist@example.com' \
-H 'Authorization: Token SecretArchiverAPIKey'
200
④ Postorius Web 面:根路径 301 落到列表索引,Postorius 列表索引页与列表详情页 200,归档消息页 200,静态资源 200(实测回显):
curl -sI http://127.0.0.1/ | grep -i location
curl -s -o /dev/null -w 'postorius lists: %{http_code}\n' http://127.0.0.1/postorius/lists/
curl -s -o /dev/null -w 'postorius list detail: %{http_code}\n' http://127.0.0.1/postorius/lists/testlist.example.com/
curl -s -o /dev/null -w 'archived message page: %{http_code}\n' \
'http://127.0.0.1/hyperkitty/list/testlist@example.com/message/7XRGOTDNFHGWA2TGKVYEUD7SMRUDH3TC/'
curl -s -o /dev/null -w 'static css: %{http_code}\n' http://127.0.0.1/static/django-mailman3/css/main.css
location: /mailman3/lists/
postorius lists: 200
postorius list detail: 200
archived message page: 200
static css: 200
且列表索引页 HTML 中可见刚建的列表(实测 grep 节选):
testlist.example.com/">Testlist
testlist@example.com
⑤ 登录验证:/accounts/login/ 返回登录页 200,用 admin / q1w2e3r4 提交后 302 跳转即登录成功(实测回显;带 CSRF token 的完整 POST):
curl -s -o /dev/null -w 'login page: %{http_code}\n' http://127.0.0.1/accounts/login/
# 浏览器访问 http://pm1.example.com/,用 admin / q1w2e3r4 登录,
# 应能看到 testlist@example.com,进入列表设置/Held Message/订阅者管理等 Postorius 管理页。
login page: 200
至此在线方案安装全部完成。Postorius 的日常列表管理操作(建列表、订阅审核、Held Message 处理等)见 API 使用详解篇。
五、离线安装方案(B:wheel 离线包)
5.1 方案说明
目标服务器无法访问 PyPI 时(内网/隔离区),在一台能联网的机器上用 pip download 把全套 wheel 与依赖一次性取回,打包带入内网安装。Python 系包离线分发的标准形态就是 wheel 池 + --no-index --find-links,后续 4.5–4.11 的配置与验证步骤与方案 A 完全相同,本章只覆盖差异部分。
⚠️ 本套件离线分发的特殊点(实测踩过):django-haystack 3.4.0 在 PyPI 只有 sdist 没有 wheel,离线安装时要现场构建,wheel 池必须额外带上
setuptools、setuptools_scm[toml]>=3.4、wheel三个构建依赖,否则--no-index下构建依赖取不到,安装直接失败(实测报No matching distribution found for setuptools>=61.2,补齐后一次通过)。
5.2 联网机:下载离线包
mkdir -p /opt/pm-offline
/opt/mailman/venv/bin/pip download -d /opt/pm-offline \
"mailman==3.3.10" "mailman-hyperkitty==1.2.1" \
"postorius==1.3.13" "django-mailman3==1.3.15" \
"mailman-web==0.0.9" "django-haystack==3.4.0" "whoosh==2.7.4" \
"psycopg2-binary==2.9.13" "alembic<1.15" "sqlalchemy<2.0" "falcon<4" \
"setuptools" "setuptools_scm[toml]>=3.4" wheel
完成后核验(实测回显,共 83 个文件、53 MB,含全部传递依赖与构建依赖):
ls /opt/pm-offline | wc -l
du -sh /opt/pm-offline
83
53M /opt/pm-offline
其中关键构件(实测核对):postorius-1.3.13-py3-none-any.whl、mailman-3.3.10-py3-none-any.whl、falcon-3.1.3-cp310-cp310-manylinux…whl、django_haystack-3.4.0.tar.gz(唯一 sdist)、setuptools-84.2.1-py3-none-any.whl。
5.3 离线机:安装与验证
把 /opt/pm-offline 整目录拷到目标机(scp/rsync/U 盘均可),然后:
python3 -m venv /opt/mailman/venv
/opt/mailman/venv/bin/pip install --no-index --find-links /opt/pm-offline \
"mailman==3.3.10" "mailman-hyperkitty==1.2.1" \
"postorius==1.3.13" "django-mailman3==1.3.15" \
"mailman-web==0.0.9" "django-haystack==3.4.0" "whoosh==2.7.4" \
"psycopg2-binary==2.9.13" "alembic<1.15" "sqlalchemy<2.0" "falcon<4"
实测回显(Successfully installed 行节选,与在线方案版本完全一致):
Successfully installed Mako-1.4.3 ... django-4.2.30 ... falcon-3.1.3 ... hyperkitty-1.3.12 ... mailman-3.3.10 mailman-hyperkitty-1.2.1 mailman-web-0.0.9 mailmanclient-3.3.5 ... postorius-1.3.13 psycopg2-binary-2.9.13 ... sqlalchemy-1.4.54 ... whoosh-2.7.4 ...
--no-index 强制不访问任何网络源,只从本地 wheel 池解析。验证方式与 4.4 节完全一致(两条 pip show 命令,版本输出逐一比对)。
5.4 与方案 A 的差异说明
离线方案仅替换 4.4 节的「pip 在线安装」一步;4.2 的系统 apt 包(postgres/nginx/uwsgi/postfix/sassc/curl)与 4.3 的数据库初始化仍需在目标机完成(apt 侧离线用 apt-get install --download-only 预取 deb 包),4.5 起的全部配置、托管、验证步骤逐字相同。
六、方案对比与选择建议
| 维度 | 方案 A:在线 venv + pip | 方案 B:离线 wheel 包 |
|---|---|---|
| 是否需联网 | 安装机需访问 PyPI | 安装机全程离线,联网机代取 |
| 版本控制 | 命令内限版,实时解析依赖 | wheel 池固化,离线机被动一致 |
| 安装速度 | 受网络波动影响 | 本地盘秒级装完 |
| 依赖一致性 | 同一时刻解析,各机可能漂移 | 一份 wheel 池多机复用,严格一致 |
| 适用场景 | 单台/少量服务器、可出网 | 内网隔离、批量交付、安全审计环境 |
选择建议:能出网就选方案 A(命令最少、始终拿到最新安全补丁);内网隔离或多台同版本交付选方案 B,且 wheel 池建好后把 /opt/pm-offline 纳入配置管理归档,未来扩容直接复用。
七、配置说明
安装完成后值得复核的安全与运行配置(均为 4.5–4.8 节已写入内容的清单化汇总):
- 防火墙:对外只需 80(Web)与 25(SMTP);8000/8001/5432 已分别绑 127.0.0.1,不应放行公网。用 nftables/ufw 做默认拒绝时,示例放行集即
80,25/tcp; - SECRET_KEY:
/etc/mailman3/settings.py中替换为 ≥50 位随机串(python3 -c "import secrets; print(secrets.token_urlsafe(50))"生成),更换后所有会话失效,需重新登录; - DEBUG=False:已默认关闭;排障时临时开启务必改回,调试页会暴露配置与栈信息;
- ALLOWED_HOSTS:只放真实对外域名(本例
pm1.example.com),多域名逗号分隔追加; - REST 凭据:
mailman.cfg [webservice]与settings.py的MAILMAN_REST_API_*成对修改,改完systemctl restart mailman3 mailman-web——这是 Postorius 的数据通道,务必改掉演示口令; - 归档令牌:
mailman-hyperkitty.cfg的api_key与settings.py的MAILMAN_ARCHIVER_KEY成对修改; - Whoosh 索引:
/opt/mailman/mm/fulltext_index首次搜索前由mailman-web update_index建全量,之后建议以定时任务每日刷新,该目录纳入备份范围; - 备份最小集:
/etc/mailman3/(三份配置)+/opt/mailman/mm/var/(core 运行态)+ 两个数据库(mailman、mailmanweb)+/opt/mailman/mm/static可再生不用备。
收工自检清单(全部应有实测通过记录):systemctl is-active 五服务全 active(4.9 回显)、mailman info 版本与 db url 正确(见下)、nginx 入口 301/200(4.11④)、spool 为 0(4.11②)。
su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman -C /etc/mailman3/mailman.cfg info'
GNU Mailman 3.3.10 (Tom Sawyer)
Python 3.10.12 (main, Aug 31 2026, 10:18:17) [GCC 11.4.0]
config file: /etc/mailman3/mailman.cfg
db url: postgresql+psycopg2://mailman:q1w2e3r4@localhost:5432/mailman
devmode: DISABLED
REST root url: http://localhost:8001/3.1/
REST credentials: restadmin:restpass
八、常见问题(FAQ)
Q1:REST API 建列表/建域报 400 Missing Parameter: fqdn_listname,参数明明传了?
根因:mailman 3.3.10 声明的依赖是 falcon>=3.1.3(无上限),2026 年 pip 默认会装 falcon 4.x;而 mailman 的 REST 参数解析依赖 falcon 3 的 auto_parse_form_urlencoded 机制(wsgiapp.py 显式开启),falcon 4 已移除该选项,导致所有表单 POST 的 body 参数不被解析。实测:falcon 4.4.0 下 -d fqdn_listname=… 稳定复现 400 Missing Parameter。解决:pip install "falcon<4" 降到 3.1.3,systemctl restart mailman3。4.4 节安装命令已带限版。另注意 -F(multipart)在本套件任何 falcon 版本下都不被解析,REST 命令一律用 -d。
Q2:core 启动报数据库方言无法识别 / mailman info 报错退出?
定位:mailman.cfg 的 url 写成了裸 postgresql://。解决:改为 postgresql+psycopg2:// 前缀(显式指定 SQLAlchemy 驱动),改完 systemctl restart mailman3,mailman info 能打印 db url 即恢复。这是 Mailman + PostgreSQL 组装最高频的一错。
Q3:第一条 mailman-web 命令就崩,报 ValueError: Unable to configure handler 'file'?
根因:mailman-web 0.0.9 的 BASE_DIR 固定为 /opt/mailman/web,其日志 handler 启动即要打开 /opt/mailman/web/logs/mailmanweb.log,目录不存在时 logging 配置阶段直接抛错。解决:mkdir -p /opt/mailman/web/logs && chown -R mailman:mailman /opt/mailman(4.4 节已纳入标准步骤),重跑原命令即可。
Q4:nginx 全站 502,uwsgi.sock 存在?
定位:/etc/uwsgi/mailman-web.ini 只写了 http-socket,没写 socket=——nginx 的 uwsgi_pass 说的是 uwsgi 二进制协议,HTTP socket 应答不了。解决:socket = /opt/mailman/mm/uwsgi.sock 与 chmod-socket = 666 两行都要有(666 保证 nginx 的 www-data 用户可读写该 sock),systemctl restart mailman-web 后 curl -I http://127.0.0.1/ 返回 301。
Q5:core 推送归档报 RemoteDisconnected,邮件落 spool?
定位:反向踩坑——为修 Q4 只保留了 uwsgi 协议 socket,删掉了 http-socket,而 mailman-hyperkitty 插件是 HTTP 客户端,连不上纯 uwsgi 协议端口。解决:socket= 与 http-socket = 127.0.0.1:8000 同时保留(nginx 走前者、归档推送走后者),重启 mailman-web;无需人工处理 spool,core 会自动重试补归档。
Q6:REST 建列表报 400 Domain does not exist: example.com?
根因:Mailman 要求邮件域先注册后建列表。解决:先 POST /3.0/domains(-d mail_host=example.com)拿到 201,再 POST /3.0/lists(4.11 第一步的顺序)。用 Postorius 界面操作时同理,先在 Domains 页建域。
Q7:pip 装完 core 起不来,日志见 SQLAlchemy/alembic 相关报错?
根因:SQLAlchemy 2.x 与 alembic 1.15+ 的 API 变更破坏了 mailman 3.3.10 的兼容。解决:pip install "alembic<1.15" "sqlalchemy<2.0" 降级(本方案落到 1.14.1 / 1.4.54),重启 mailman3。4.4 节的安装命令已带限版,从源头规避。
Q8:mailman-web compress 报错找不到 sassc?
现象:CommandError: The program 'sassc' ...。解决:apt install -y sassc 后重跑 compress --force,成功输出含 Compressed 2 block(s) from 80 template(s) for 1 context(s).(实测)。4.2 节已把它列入系统依赖。
九、卸载与清理
按依赖反向顺序拆除,数据库数据先备份再删:
# 1. 停服务并取消自启
systemctl disable --now mailman3 mailman-web
# 2. 备份配置与运行态(按需)
tar czf /root/pm-backup.tar.gz /etc/mailman3 /opt/mailman/mm/var /opt/mailman/mm/fulltext_index
# 3. 删运行目录与虚拟环境
rm -rf /opt/mailman
# 4. 删 uwsgi/nginx 配置
rm -f /etc/uwsgi/mailman-web.ini /etc/nginx/sites-enabled/mailman-web /etc/nginx/sites-available/mailman-web
systemctl reload nginx
# 5. 删数据库与用户
su - postgres -c "psql -c 'DROP DATABASE IF EXISTS mailman;'"
su - postgres -c "psql -c 'DROP DATABASE IF EXISTS mailmanweb;'"
su - postgres -c "psql -c 'DROP ROLE IF EXISTS mailman;'"
# 6. 删系统用户
userdel mailman
# 7.(可选)apt 侧组件若为他用则保留;确认本方案独占时再 purge
apt purge -y uwsgi uwsgi-plugin-python3
复核无残留:systemctl list-unit-files | grep mailman 应无输出、ls /opt/mailman /etc/mailman3 应报不存在、psql 双库已删。
十、参考链接
- Postorius 官方安装文档:https://postorius.readthedocs.io/en/latest/install.html
- Mailman 3 官方安装与部署总览:https://docs.mailman3.org/en/latest/install/virtualenv.html
- Mailman core 文档(mailman.cfg 布局与 MTA 集成):https://mailman.readthedocs.io/en/latest/
- PyPI 项目页(Postorius 版本与依赖):https://pypi.org/project/postorius/
- GitLab 源码仓库:https://gitlab.com/mailman/postorius
- 站内参考:《Postorius 1.3.13 介绍与选型指南》、《Mailman 3.3.10 介绍与选型指南》、《Ubuntu 22.04 安装 Mailman 指南》
(实测环境与全部回显采集日期:2026-10-26)