在 Mailcow 管理后台中逐个创建邮箱账号并不困难,但当一次需要创建十几个甚至更多账号时,重复填写用户名、密码、配额和启用状态会变得繁琐,也容易出现输入错误。

Mailcow 提供了管理 API,可以通过脚本批量创建邮箱。即使 Mailcow 使用 Docker 部署,调用方也不需要进入容器,只需通过 Mailcow 对外提供的 HTTPS 地址访问 API。

本文介绍一种相对安全、简单且容易检查的批量创建方法。


一、Mailcow API 是什么

Mailcow 的网页管理后台适合人工操作,而 API 适合程序化管理。

例如,创建邮箱通常使用类似下面的接口:

POST https://mail.example.com/api/v1/add/mailbox

请求中包含:

  • 邮箱所属域名
  • 邮箱用户名
  • 显示名称
  • 密码
  • 邮箱配额
  • 是否启用
  • TLS 等附加设置

调用成功后,Mailcow 会像在网页后台手动创建一样,在系统中生成邮箱账号。

API 客户端与 Docker 容器之间没有直接关系,实际通信过程如下:

运行脚本的电脑
        │
        │ HTTPS + API Key
        ▼
Mailcow 管理 API
        │
        ▼
Mailcow 内部完成邮箱创建

因此,脚本可以运行在:

  • Mailcow 所在服务器
  • 同一局域网内的管理电脑
  • 其他能够访问 Mailcow 域名的可信主机

不需要进入 Docker 容器,也不需要直接操作数据库。


二、在 Mailcow 后台启用 API

登录 Mailcow 管理后台,进入系统配置中的 API 页面。

通常可以看到两类权限:

  • Read-Only Access:只读 API
  • Read-Write Access:读写 API

创建邮箱属于写操作,因此需要临时启用 Read-Write API。

建议遵循以下原则:

  1. 只在批量操作期间启用读写 API。
  2. 尽量限制允许访问 API 的来源 IP。
  3. 不要长期启用“跳过 IP 检查”。
  4. API Key 不要写入脚本。
  5. 操作完成后关闭读写 API,必要时重新生成密钥。

API Key 的权限通常接近管理员权限,应当按照高敏感凭据处理。


三、为什么使用 Python 脚本

理论上,可以直接使用 curl 调用 Mailcow API。

例如:

curl -X POST \
  -H "X-API-Key: API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.com","local_part":"user01"}' \
  https://mail.example.com/api/v1/add/mailbox

但真实密码通常包含:

$
&
#
%
!
(
)
_
-
=

如果直接把大量复杂密码写进 Shell 命令,很容易遇到:

  • Shell 变量展开
  • 引号提前结束
  • JSON 转义错误
  • 反斜杠被错误处理
  • 实际密码与原始密码不一致

Python 自带的 json 模块可以自动处理 JSON 转义,因此适合批量提交包含复杂密码的数据。

这并不表示 Mailcow 依赖 Python。Python 只是 API 客户端,真正执行邮箱创建的仍然是 Mailcow API。


四、准备邮箱账号数据

假设需要创建三个邮箱:

account01@example.com
account02@example.com
account03@example.com

每个邮箱使用不同的随机密码,并分配相同的小容量配额。

建议密码只使用兼容性较好的 ASCII 字符,例如:

abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
!@#$%^&*()-_=+

应避免使用容易混淆或可能在不同环境中被转换的字符,例如:

  • 中文或全角标点
  • 弯引号 “ ”
  • 不可见字符
  • 换行符
  • 制表符

ASCII 单引号、双引号和反斜杠并非不能作为密码字符,但会增加脚本和配置文件中的转义复杂度。普通用途下,没有必要刻意使用这些字符。


五、批量创建脚本示例

下面是一份简化后的 Python 示例。

其中的域名、邮箱名和密码均为演示数据,实际使用时应替换。

#!/usr/bin/env python3

import json
import os
import sys
import urllib.error
import urllib.request

MAILCOW_URL = "https://mail.example.com"
DOMAIN = "example.com"
QUOTA_MB = "20"

API_KEY = os.environ.get("MAILCOW_API_KEY")

if not API_KEY:
    print("错误:未设置 MAILCOW_API_KEY", file=sys.stderr)
    sys.exit(1)

mailboxes = [
    ("account01", "Example-Pass_01!Ab"),
    ("account02", "Example-Pass_02!Cd"),
    ("account03", "Example-Pass_03!Ef"),
]

endpoint = f"{MAILCOW_URL.rstrip('/')}/api/v1/add/mailbox"

success_count = 0
failed_count = 0

for local_part, password in mailboxes:
    address = f"{local_part}@{DOMAIN}"

    payload = {
        "active": "1",
        "domain": DOMAIN,
        "local_part": local_part,
        "name": local_part,
        "password": password,
        "password2": password,
        "quota": QUOTA_MB,
        "force_pw_update": "0",
        "tls_enforce_in": "1",
        "tls_enforce_out": "1",
    }

    request = urllib.request.Request(
        endpoint,
        data=json.dumps(payload).encode("utf-8"),
        method="POST",
        headers={
            "X-API-Key": API_KEY,
            "Content-Type": "application/json",
            "Accept": "application/json",
        },
    )

    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            body = response.read().decode("utf-8", errors="replace")

        result = json.loads(body)
        entries = result if isinstance(result, list) else [result]

        succeeded = any(
            isinstance(entry, dict)
            and entry.get("type") == "success"
            for entry in entries
        )

        if succeeded:
            print(f"[成功] {address}")
            success_count += 1
        else:
            print(f"[失败] {address}")
            print(json.dumps(result, ensure_ascii=False, indent=2))
            failed_count += 1

    except urllib.error.HTTPError as error:
        body = error.read().decode("utf-8", errors="replace")
        print(f"[HTTP {error.code}] {address}")
        print(body)
        failed_count += 1

    except Exception as error:
        print(f"[异常] {address}: {error}")
        failed_count += 1

print()
print(f"完成:成功 {success_count} 个,失败 {failed_count} 个")

sys.exit(1 if failed_count else 0)

六、运行脚本

将脚本保存为:

create_mailboxes.py

先限制文件权限:

chmod 600 create_mailboxes.py

然后通过隐藏输入的方式读取 API Key:

read -rsp '请输入 Mailcow 读写 API Key: ' MAILCOW_API_KEY
echo
export MAILCOW_API_KEY

执行脚本:

python3 create_mailboxes.py

执行完成后立即清除环境变量:

unset MAILCOW_API_KEY

成功时会看到类似输出:

[成功] account01@example.com
[成功] account02@example.com
[成功] account03@example.com

完成:成功 3 个,失败 0 个

退出状态为 0,通常表示整批创建成功。


七、为什么不把 API Key 写进脚本

不应采用下面这种写法:

API_KEY = "真实密钥"

原因包括:

  • 脚本可能被同步到云盘
  • 文件可能进入备份
  • 可能被提交到 Git 仓库
  • 其他本地用户可能读取
  • 后续分享脚本时容易忘记删除密钥

通过环境变量临时注入,可以让脚本本身不包含 API Key。

操作完成后执行:

unset MAILCOW_API_KEY

即可清除当前 Shell 环境中的变量。

需要注意的是,邮箱密码仍然写在批量创建脚本中。因此脚本使用完毕后,应当删除或转移到受保护的加密存储中。

rm -f create_mailboxes.py

八、常见失败原因

1. API Key 权限不足

只读 API Key 无法创建邮箱。

应使用临时启用的 Read-Write API Key。

2. 来源 IP 不在允许列表

Mailcow 可以限制哪些 IP 有权调用 API。

如果调用方不在允许范围内,请求会被拒绝。

3. 邮箱已经存在

同一个邮箱地址不能重复创建。

脚本会把该账号记录为失败,但一般不会影响其他账号。

4. 域名配额不足

每个邮箱的配额会计入域名总配额。

例如创建十个每个 20 MB 的邮箱,最多会占用:

10 × 20 MB = 200 MB

如果域名剩余配额不足,部分邮箱可能创建失败。

5. 密码不符合策略

Mailcow 可能要求密码满足长度或复杂度规则。

使用长度足够、包含大小写字母、数字和常用符号的随机密码,一般可以避免这一问题。

6. 地址配置错误

脚本中的:

MAILCOW_URL = "https://mail.example.com"

应当只填写 Mailcow 的基础域名。

不要填写:

https://mail.example.com/admin/system

因为 /admin/system 是网页后台路径,而不是 API 基础地址。


九、执行后的安全收尾

创建完成后,应当完成以下收尾工作:

  1. 在 Mailcow 后台确认所有邮箱都已出现。
  2. 检查邮箱配额是否正确。
  3. 关闭 Read-Write API。
  4. 必要时重新生成 API Key。
  5. 清除终端中的环境变量。
  6. 删除包含明文密码的批量脚本。
  7. 将账号和密码保存到密码管理器。
  8. 不要把账号密码保存到公开文档或普通聊天记录。

十、总结

Mailcow 的 Docker 部署方式不会妨碍远程 API 管理。只要 Mailcow 的 HTTPS 地址可以访问,并且调用方持有有效的读写 API Key,就可以从任意可信设备批量创建邮箱。

整个过程的核心是:

准备账号数据
    ↓
临时启用 Read-Write API
    ↓
通过环境变量提供 API Key
    ↓
Python 将账号数据转换为 JSON
    ↓
调用 Mailcow add/mailbox API
    ↓
逐个检查成功或失败
    ↓
关闭 API 并清理敏感文件

与直接修改数据库相比,使用 Mailcow API 更安全,也更符合系统本身的管理方式。对于需要批量创建独立邮箱账号的场景,这是一种简单、可控且便于审计的方法。

Leave a Reply

Your email address will not be published. Required fields are marked *