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。

Postorius2026/10/26

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
  • 端口规划(本机监听):
端口服务绑定用途
25postfix0.0.0.0收取列表邮件,LMTP 转投 core
80nginx0.0.0.0对外 Web 入口(Postorius/HyperKitty)
8000uwsgi(http-socket)127.0.0.1Web 应用(归档推送 API 走这里)
8001mailman core REST127.0.0.1Mailman 管理 API(Postorius 读列表数据)
5432PostgreSQL127.0.0.1双数据库:core 用 mailman,Web 用 mailmanweb
  • 目录规划:
路径用途
/opt/mailman/venvPython 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/staticDjango collectstatic 产物(nginx 直接托管)
/opt/mailman/mm/fulltext_indexWhoosh 全文索引目录
/etc/uwsgi/mailman-web.iniuwsgi 应用配置

⚠️ 网络说明:本篇全部软件来自 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 介绍与选型指南》。

配套组件版本矩阵(本次实测组合,亲测可用):

组件版本说明
mailman3.3.10core 引擎,REST 端口 8001
postorius1.3.13本篇主角,列表管理 Web 界面
hyperkitty1.3.12归档 Web 应用,随 mailman-web 同栈安装
django-mailman31.3.15套件公共库(账号/主题/列表元数据)
mailman-web0.0.9Django 工程模板包,提供 mailman-web 管理命令与 settings 基座
mailmanclient3.3.5Postorius/HyperKitty 访问 core REST 的客户端库
django-haystack3.4.0搜索抽象层
Whoosh2.7.4纯 Python 全文搜索引擎(零外部服务依赖)
psycopg2-binary2.9.13PostgreSQL 驱动
alembic / SQLAlchemy1.14.1 / 1.4.54⚠️ 必须限制在 1.14.x / 1.4.x,见下方依赖链说明
falcon3.1.3⚠️ 必须限制 <4,见下方依赖链说明

⚠️ 依赖链两个关键坑(均为实测踩中):

  1. mailman 3.3.10 对 SQLAlchemy 2.x 与 alembic 1.15+ 不兼容(core 初始化直接报错退出),pip 安装必须带 "alembic<1.15" "sqlalchemy<2.0";
  2. 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/200Postorius 列表索引(mailman3 是 Postorius 的官方别名前缀)
/postorius/lists/200Postorius 原生前缀,同一套视图
/accounts/login/200django-allauth 登录页
/hyperkitty/200HyperKitty 归档前端(/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 双库已删。

十、参考链接

(实测环境与全部回显采集日期:2026-10-26)