Phase 5 code is complete, committed (b2692e4) and verified on the server.
Everything at SelfPost's boundary is proven (auth, sender binding, no open
relay, valid DKIM signing, delivery to the recipient MX over TLS). The one
open item — the receiver accepting the message into its inbox — is gated by
mc.mixfed.ru's DNS cache / own-domain policy, not a relay defect; a background
loop retries until it lands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
32 KiB
Прогресс реализации SelfPost
Живой трекер фаз. Переживает /clear — читается первым при возобновлении работы.
План фаз: implementation-plan.md. ТЗ: specification.md.
Как возобновить после сброса контекста
- Прочитать этот файл (текущая фаза, статус, что сделано, что дальше).
- Прочитать соответствующую фазу в
implementation-plan.md. - При необходимости — детали в
specification.md. - Продолжить с пункта «Следующий шаг».
Рекомендуемая модель по фазам
| Фаза | Модель | Почему |
|---|---|---|
| 0 — каркас + спайк milter | Opus | архитектура + главный технический риск (7.3) |
| 1 — Docker/supervisord/обёртка | Opus | тонкая логика холодного старта сокетов |
| 2 — SQLite/setup-link/auth | Opus | безопасность 7.6 (крипто-токен, сессии, bcrypt) |
| 3 — домены + OpenDKIM | Opus | генерация конфигов + exec-safety (7.6.3–4) |
| 4 — приложения + SASL + sender_login_maps | Opus | риск open relay / привязки отправителя |
| 5 — полный Postfix | Opus | самый чувствительный тракт доставки |
| 6 — journal-milter | Opus | наивысший риск (баг ломает релей) |
| 7 — UI мониторинга | Sonnet | шаблоны/CRUD, рутинно |
| 8 — rate limit L2 | Opus | логика лимитов в milter |
| 9 — бэкап/restore/экспорт | Opus | целостность данных, версионирование |
| 10 — деплой + docs | Sonnet | compose-файлы и документация |
| 11 — security-проход | Opus | аудит соответствия 7.6 |
Правило: безопасность / инфра / риск-критичное → Opus; UI / документация / бойлерплейт → Sonnet; тривиальная механика → Haiku.
Коммиты
Коммит на каждом осмысленном шаге (не каждое сохранение файла, но и не только конец фазы): рабочий под-функционал, зелёная сборка, конец фазы. Минимум — один коммит на закрытую фазу + промежуточные на связные под-шаги. Ветка main (если пользователь не попросит отдельную). Push/PR — только по явной команде. Сообщение коммита завершается трейлером Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>.
Протокол закрытия фазы
Перед /clear в конце каждой фазы Claude:
- Обновляет этот файл: статус фазы → ✅, заполняет «Сделано» и «Следующий шаг».
- Проверяет критерии «Готово, когда…» из плана.
- Пишет одну строку в журнал ниже.
- Делает финальный коммит фазы.
- Явно говорит: «Фаза N закрыта — можно
/clear, следующая фаза N+1 на модели X».
Текущее состояние
- Текущая фаза: Фаза 5 — код завершён, закоммичен (
b2692e4) и проверен на сервере; ⏳ ждём единственного финального подтверждения — реальной доставки в ящик сdkim=pass(упирается в кэш DNS у получателя, см. ниже). После подтверждения → закрыть Фазу 5, следующая Фаза 6 (journal-milter) на Opus. - Что осталось: фоновый цикл повторов на сервере (
/tmp/p5retry.sh, лог/root/p5retry.log, контейнерp5держать живым — его ключ mixeme.ru опубликован) шлётselfpost@mixeme.ru → selfpost@mixeme.ruкаждые ~25 мин и остановится, как толькоmc.mixfed.ruпримет письмо. Приёмник пока отбивает554 does not meet our delivery requirements: наш DKIM валиден (opendkim-testkey: key OK), SPF теперь авторизует наш IP81.30.105.2, но у зоны mixeme.ru negative-TTL 3ч (SOA min 10800) и старый SPF TTL 24ч — резолвер получателя, вероятно, ещё отдаёт до-публикационные записи. Альтернатива, если это не кэш, а политика «own-domain» приёмника: отправить на нейтральный ящик (Gmail) — примет по DKIM/DMARC-выравниванию. Со стороны SelfPost всё доказано (см. «Сделано в Фазе 5»); незакрытый пункт — свойство приёмника, не дефект релея.
Сделано в Фазе 5
- Генерация конфига релея из env (
build/postfix-config.sh, вызывается изentrypoint.shпод root на каждом старте — не в образе, чтобы cert-пути/лимиты/hostname/587 задавались env и пересобирались как остальное состояние).main.cfчерезpostconf -e, сервисыmaster.cfчерезpostconf -M/-P, финальныйpostfix check. master.cf:smtps465 (implicit TLS,smtpd_tls_wrappermode=yes) — основной;submission587 (STARTTLS,smtpd_tls_security_level=encrypt) — опционально поSUBMISSION_ENABLE=true(иначе удаляетсяpostconf -MX). Обаchroot=n+smtpd_client_restrictions=permit_sasl_authenticated,reject.- SASL:
smtpd_sasl_type=cyrus, conf/etc/postfix/sasl/smtpd.conf(auxprop/sasldb,sasldb_path=/data/sasl/sasldb2). Рабочий реалм:smtpd_sasl_local_domainпустой +myhostname=$SELFPOST_HOSTNAME→ клиент логинится «голым» логином, Cyrus резолвит по realm=myhostname (= realm учётки), аsasl_usernameдля карты = голый логин = значение карты. Проверено (235 + отправка). - Привязка/анти-релей:
smtpd_sender_login_maps=texthash:/data/postfix/sender_login_maps+reject_sender_login_mismatch;smtpd_relay_restrictions/smtpd_recipient_restrictions=permit_sasl_authenticated, reject_unauth_destination; нетpermit_mynetworks— авторизация только по кредам. Open relay невозможен. - TLS/доставка: cert/key из
TLS_CERT_FILE/TLS_KEY_FILE;smtp_tls_security_level=may(исходящая, MX-lookup напрямую); суточныйpostfix reloadдля подхвата обновлённых сертов (build/postfix-cert-reload.sh,[program:cert-reload]под supervisord, exit≠0 не роняет контейнер). - Rate-limit L1:
smtpd_client_message_rate_limit/anvil_rate_time_unitиз env (дефолт 100/3600с). - Milter-цепочка (per-milter действия, синтаксис скобок Postfix 3.0):
{ unix:opendkim.sock, default_action=tempfail }, { unix:journal.sock, default_action=accept }— OpenDKIM строго (при недоступности → defer), journal fail-open (не блокирует релей, ТЗ 7.3).non_smtpd_miltersпустой. - Два инфра-фикса, найденных на сервере (см. память postfix-chroot-and-milter-socket-perms): (1)
postconf -F '*/*/chroot=n'— chroot-агент доставки не читает/etc/resolv.conf, MX-lookup падал «Host not found», письма не уходили; (2)entrypoint.shставит/run/opendkimи/run/selfpostв группуselfpost+setgid2750, journal-stub делаетchmod 0660на сокет — иначеpostfixне мог подключиться к milter-сокетам, а строгий OpenDKIM отбивал всю почту (milter-reject451). Journal-stub (cmd/panel/journalstub.go) — единственное изменение Go. - Проверено на сервере (selfpost.mixfed.ru):
gofmt/vet/testзелёные, образ собирается; контейнерный e2e — 465 auth+отправка подписано DKIM (d=домен; s=selfpost), 587 STARTTLS auth, кросс-доменный отправитель553 not owned, list-режим по адресам, неаутентифицированный релей554, реальная исходящая доставка до MX получателя по TLS (relay=mc.mixfed.ru), DKIM-подпись верифицируется опубликованным ключом (opendkim-testkey: key OK). - Не закрыто: финальный приём письма в ящик получателем
mc.mixfed.ru(кэш DNS / own-domain-политика приёмника — не дефект SelfPost). Автоповтор запущен.
Сделано в Фазе 4
- Модель приложения (
internal/store/applications.go): CRUD надapplications/application_addressesв транзакциях;loginглобально уникален (арбитр — UNIQUE,ErrLoginExists);ListBindings(SQLUNION ALL) отдаёт пары «адрес→логин» (wildcard →@domain, list → каждый адрес) детерминированно отсортированными — сырьё для карты;ListLoginsByDomainдля очисткиsasldb2перед каскадом;DeleteApplicationвозвращает удалённую строку (нужен логин дляsasldb2). - SASL
sasldb2(internal/app/sasl.go): обёртка надsaslpasswd2(эквивалент по ТЗ 5.1).Set=-p -c -f <db> -u <realm> <login>, пароль только через stdin (не в argv → не течёт вps/логи), логин — отдельный argv-элемент после строгого whitelist (validateLogin, без@— это разделитель realm вsasldb2), без shell (ТЗ 7.6.3).Delete=-d ..., идемпотентно.run— инъектируемое поле для тестов. - Пароль приложения (
internal/app/password.go): 24 байтаcrypto/rand→ base64url (192 бита), генерируется панелью, показывается один раз, plaintext не хранится (ТЗ 7.6.1). - Валидация (
internal/app/validate.go):validateLogin(3–64,[A-Za-z0-9._-], без@);validateSenderAddress— критичная проверка ТЗ 7.6.2: часть после@обязана строго равняться домену приложения (проверка ДО записи в конфиг, а не черезsender_login_mapsпри доставке); строгий whitelist localpart;parseAddressesнормализует/дедуплицирует/требует ≥1 адрес. smtpd_sender_login_maps(internal/postfix/postfix.go): полная регенерация файла из всех привязок (чистая функция реестра, идемпотентно, как OpenDKIM-таблицы), атомарная запись (write.go, temp+rename). Many-to-one: несколько логинов на один адрес сливаются в одну строкуадрес log1,log2(штатный случай ТЗ 5.1 §4).assertMapSafe— backstop против пробелов/переводов строк/запятых/@в логине (ТЗ 7.6.4). Тип карты для Фазы 5 —texthash:(безpostmap).- Reload Postfix — исправленный механизм (ключевое инфра-решение): изначальный план «
supervisorctl signal HUP postfix» не работает —postfix start-fgфоркает отдельный master, и сигнал супервизируемому foreground-процессу до master не доходит (в отличие от OpenDKIM, который сам и есть foreground-процесс). Решение: одноразовая supervisord-программа[program:postfix-reload](command=/usr/sbin/postfix reload,autostart=false,startsecs=0,exitcodes=0); панель дёргает еёsupervisorctl start postfix-reloadчерез тот же групповой контрол-сокет. Даёт настоящийpostfix reloadот root без привилегий у панели; postfix остаётся RUNNING, программа уходит в EXITED(0), crashexit не срабатывает. Проверено поmail.log(reload -- version). - Сервис приложений (
internal/app/service.go): координация store+sasldb2+карты.Create— сначала строка реестра (арбитр дубля, не даёт затереть чужой пароль вsasldb2), затемsasldb2, затем rebuild карты + reload; полный откат при сбое любого шага.UpdateMode/Delete/RegeneratePassword/PurgeDomainSASL/Resync.SenderMaps— интерфейс над Postfix для тестируемости. - Интеграция с доменами (
internal/domain/service.go):Deleteтеперь через интерфейсApplications— сначалаPurgeDomainSASL(пока логины в реестре), затем каскад БД, затем rebuild карты + reload Postfix, затем удаление DKIM-ключа. Ручной reload (7.2.12) теперь перегружает и OpenDKIM, и Postfix. - Web (
internal/web/handlers_apps.go, шаблонdomain_detail.html): создание/редактирование режима/перевыпуск/удаление приложения в карточке домена; одноразовый показ пароля рендерится инлайн (не через redirect — иначе пароль потерян); серверные ошибки валидации показываются на форме; флаги подтверждения на удаление/перевыпуск. - Инфра:
postfixдобавлен в группуselfpost(Dockerfile) для чтенияsasldb2/карты;entrypoint.shнормализует/data/sasl(setgid 2750,sasldb20640) и/data/postfix(файл карты создаётся пустым до старта, 0640) — самолечение прав после restore. - Проверено на сервере (selfpost.mixfed.ru):
gofmt/go vet/go testзелёные (юниты: store CRUD/bindings/cascade, рендер карты+инъекции, валидация логина/адреса/принадлежности домену, argv/stdinsaslpasswd2, сервис create/rollback/delete/mode/regen/purge с фейками); docker build ок; контейнерный e2e — весь жизненный цикл приложения + каскад домена + персистентность через рестарт + реальныйpostfix reloadиз пути создания и кнопки reload.
Сделано в Фазе 3
- Per-domain DKIM в чистом Go (
internal/domain/dkim.go): RSA-2048 черезcrypto/rsa, приватный ключ PKCS#1 PEM пишется атомарно (temp+rename,writeFileAtomic) с mode 0640; DNS TXT-запись (v=DKIM1; h=sha256; k=rsa; p=<base64 PKIX DER>) вычисляется из ключа на лету — ключ на диске = единственный источник истины.opendkim-genkeyне используется (никакого exec для keygen). - OpenDKIM KeyTable/SigningTable (
internal/domain/opendkim.go): полная регенерация обеих таблиц из реестра при каждом add/delete (идемпотентно), атомарная запись;KeyTable— абсолютный путь к ключу (OpenDKIM резолвит относительные от CWD — проверено),SigningTableчерезrefile:с шаблоном*@domain.assertConfigSafe(backstop 7.6.4) отклоняет пробелы/переводы строк/://в имени/селекторе перед записью. - Reload OpenDKIM без root (ключевое инфра-решение): панель (uid 999,
panel) не может сигналить процесс opendkim (uid 100) напрямую. Reload =supervisorctl signal USR1 opendkim(exec, фикс-аргументы, без shell/ввода — 7.6.3); контрол-сокет supervisord открыт группе (chown=root:selfpost chmod=0770),panelв группеselfpost.ExecReloadopendkim =kill -USR1(SIGUSR1 перечитывает таблицы) — подтверждено. - Межпользовательский доступ к ключам: общая группа
selfpost(в неё добавленыpanelиopendkim); дерево/data/opendkimс setgid (mode 2750) → файлы, созданные панелью, наследуют группуselfpost; ключи 0640 → opendkim читает по группе.RequireSafeKeys no(ключи group-readable — безопасно на приватном single-tenant bind-mount).entrypoint.shнормализует дерево на каждом старте (group/setgid/пермишены + пустые KeyTable/SigningTable до старта opendkim), самолечение после restore.opendkim.confпереведён Modev→s. - Сервис/веб (
internal/domain/service.go,internal/store/domains.go,internal/web/handlers_domains.go): Add (реестр→ключ→rebuild→reload, откат строки при сбое; существующий ключ переиспользуется, не перегенерируется — иначе сломается опубликованный DNS), Delete (каскад приложений через FKON DELETE CASCADE+ удаление ключа + rebuild+reload), список на дашборде со счётчиком приложений, карточка домена с TXT-записью (7.2.10), отдельная страница подтверждения удаления с предупреждением о каскаде (7.2.4), ручной Reload (7.2.12, пока только OpenDKIM — Postfix в Фазе 5). Валидация имени домена — строгий whitelist[a-z0-9.-], DNS-форма, ≥2 меток (7.6.2), нормализация в lower-case. Роутинг переведён на authenticated под-mux с method+wildcard-паттернами Go 1.22. - Проверено на сервере (selfpost.mixfed.ru):
gofmt/go vet/go test(юниты: validateDomain, DKIM keygen/record roundtrip, renderTables + injection-safety, EnsureKey reuse, store cascade) зелёные;docker buildок; контейнерный e2e (curl+сессия): три процесса живы, addExample.COM→нормализация→ключ+таблицы+TXT, opendkim читает ключ панели и остаётся RUNNING после reload, рестарт → ключ и таблицы персистентны (хэш совпал), delete → каскад, удаление ключа, пустые таблицы, opendkim reload; лог панели без ошибок.
Сделано в Фазе 2
- SQLite-персистентность (
internal/store): драйверmodernc.org/sqlite(чистый Go, без cgo — статик-бинарник сохранён; первые сторонние зависимости → появилисьgo.modrequire +go.sum), WAL +foreign_keys(ON)+busy_timeoutчерез DSN_pragma,MaxOpenConns(1). Встроенные (embed) нумерованные миграции с версионированием черезPRAGMA user_version; миграция0001_init.sqlзаводит всю схему ТЗ 9:admin(одна строка,CHECK id=1),settings,domains,applications,application_addresses,send_log(+индексы),rate_limits. ЗапросыAdminExists/CreateAdmin/GetAdmin. - Setup secret-link (
internal/web/setup.go, ТЗ 7.6.1): токен 128 бит изcrypto/rand(base64url), ссылкаhttps://<SELFPOST_HOSTNAME>/setup/<token>печатается в лог и пишется в/data/setup-token(0600); TTL 10 мин с перегенерацией при истечении/рестарте (пока нет админа); сравнениеsubtle.ConstantTimeCompare; неудачи НЕ инвалидируют токен; «настройка завершена» = наличие строкиadmin(источник истины), поэтому после успеха токен сгорает навсегда и весь/setup/*→ 404. Форма создания админа — одноразовая; гонка двух POST безопасна (CHECK id=1+ проверка существования). - Пароль админа — только
bcrypt(golang.org/x/crypto/bcrypt, DefaultCost); серверная валидация (username whitelist 7.6.2, пароль ≥12). - Логин + сессии (
internal/web/handlers_auth.go,session.go): вход сверяет username + bcrypt (bcrypt считается всегда — timing-инвариантно), сессии в памяти (не в списке ТЗ 9 на персист — рестарт просто разлогинивает), крипто-токен 256 бит; cookieHttpOnly/Secure/SameSite=Lax(Secureпо умолчанию, отключаетсяPANEL_COOKIE_SECURE=falseтолько для dev-HTTP); rate-limit логина (ratelimit.go, 10/15мин по IP) и отдельный на/setup(10/мин); auth-middleware защищает панель. - Front-end: базовый layout
html/template(автоэкранирование, 7.6.7) + страницы setup/login/dashboard (embed); вендоренныйhtmx.min.js2.0.4 (internal/web/static, отдаётся с/static/). - Entrypoint для bind-mount (
build/entrypoint.sh):/data— host bind mount → приходит от root, а панель работает под непривилегированнымpanel(uid 999). Точка входа под root чинит владельца/dataпередexec supervisord(иначе SQLiteunable to open database file). Найдено и исправлено при контейнерной проверке. - Проверено на сервере (selfpost.mixfed.ru):
go vet/go build/go test/gofmt -lчисто; функциональный e2e (curl): setup-ссылка печатается+в файл,GET /setup/<token>→200, неверный/просроченный→404,POST /setupсоздаёт админа→303, повторный/setup→404, файл токена удалён; плохой логин→401, хороший→303 с cookieHttpOnly; Secure; SameSite=Lax,/→200, logout→303, после logout/→303; рестарт с существующим админом не печатает setup. Docker-образ собирается; контейнер с-v ./data:/data: три процесса, БД+токен создаются подpanel, токен 0600.
Сделано в Фазе 1
- Образ
build/Dockerfile(bookworm-slim): многостадийная статическая сборка Go (CGO_ENABLED=0,go vetв сборке); рантайм — postfix, opendkim(+tools), cyrus-sasl (sasl2-bin,libsasl2-modules), supervisor, logrotate, ca-certificates;maillog_file=/var/log/mail.log; непривилегированный пользовательpanel(ТЗ 7.6.8);.dockerignore(dev/ и docs/ не попадают в контекст). - supervisord (
build/supervisord.conf,nodaemon, PID 1): порядокpriority=opendkim(100)→panel(200)→postfix(300); event-listenercrashexit.pyнаPROCESS_STATE_FATALшлёт SIGTERM супервизору → контейнер завершается при неперезапускаемом падении (ТЗ 4). - Обёртка Postfix (
build/postfix-wrapper.sh): опросtest -Sобоих milter-сокетов (OpenDKIM/run/opendkim/opendkim.sock+ journal/run/selfpost/journal.sock), таймаут 30с, при неготовности — выход ≠0 без запуска Postfix; иначеexec postfix start-fg(решает только холодный старт, ТЗ 4). - Панель (
cmd/panel, три роли под общим ctx + graceful shutdown по SIGTERM): HTTP:8080заглушка +/healthz; stub journal-milter — открывает unix-сокет, чтобы обёртка проходила (реальный milter — Фаза 6); stub log-tailer (реальный хвост — Фаза 6).opendkim.confвременно Modev(без ключей; Modes+KeyTable — Фаза 3). - Проверено на сервере (selfpost.mixfed.ru):
docker buildок;docker run→ три живых процесса (opendkim, panel@non-root, postfix master); панель/→200 и/healthz→ok; обёртка дождалась сокетов; форсированный FATAL панели → crashexit → контейнер завершился чисто; SIGTERM → «panel stopped cleanly». Образ 334MB, тегselfpost:devоставлен на сервере.
Сделано в Фазе 0
- Go-модуль
codeberg.org/mix/selfpost, каркасcmd/panel+cmd/selfpost-backup, общийinternal/buildinfo(версия через ldflags). Makefile(статическая сборкаCGO_ENABLED=0),LICENSE(полный AGPL-3.0), скелетREADME.md,.gitattributes(LF).- Проверено на сервере:
go vetчист,make buildдаёт статические бинарники, версия впечатывается. - Спайк go-milter (де-риск ТЗ 7.3): v0.4.1 (BSD-2) ↔ Postfix 3.7.11 bookworm, протокол v6. Подтверждено чтение From/To(per-rcpt)/Subject/queue-id, client IP из Connect(), fail-open при падении milter'а. Детали:
dev/spike-milter-notes.md. - Провижён dev-сервер: Go 1.26.5, Docker 29.6.1, git, rsync, make. Host-postfix (спайковый) остановлен+отключён.
Рабочая петля (dev loop) — ВАЖНО
Локально (Windows, D:\Local\Git\selfpost) нет Go и Docker — только редактирование и git. Вся сборка/тесты идут на dev-сервере selfpost.mixfed.ru (Debian 12 bookworm, тот же, что базовый образ; провижён под разработку). Цикл: править локально → rsync дерева на сервер → go build/go vet/docker build/тесты там. Источник истины и git-история — локальный репозиторий; сервер — только исполнитель сборки/тестов. Подключение: ssh root@selfpost.mixfed.ru (по ключу).
Утверждённые решения
- Module path:
codeberg.org/mix/selfpost - SQLite:
modernc.org/sqlite(чистый Go, без cgo) - milter:
github.com/emersion/go-milter(проверить совместимость в Фазе 0) - Структура: стандартная Go-раскладка (
cmd/,internal/) - Front-end:
html/template+ вендоренный HTMX - Коммиты — только по явной команде пользователя (ТЗ 12.1)
Журнал фаз
- Фаза 0 (2026-07-11, Opus) — каркас проекта + build-пайплайн + спайк go-milter (риск ТЗ 7.3 снят). Коммиты
4e589e1(каркас),87388b4(план). - Фаза 1 (2026-07-11, Opus) — Docker-образ + supervisord + три процесса, холодный старт (обёртка ждёт milter-сокеты) и crashexit проверены на сервере. Коммит
ed9e942. - Фаза 2 (2026-07-11, Opus) — SQLite (
modernc.org/sqlite, миграции, схема ТЗ 9), setup secret-link (128-бит токен, TTL 10м, const-time, одноразово), bcrypt-админ, логин/сессии/cookie-флаги, rate-limit setup+логина, html/template + вендоренный HTMX, auth-middleware; entrypoint чинит владельца bind-mount/data. Проверено на сервере (e2e curl + docker run). - Фаза 3 (2026-07-11, Opus) — домены + per-domain DKIM: keygen в чистом Go (RSA-2048, PKCS#1 PEM, TXT из ключа), OpenDKIM KeyTable/SigningTable (Mode
s,refile:) с полной регенерацией, reload черезsupervisorctl signal USR1 opendkim(сокет открыт группеselfpost, панель без root — 7.6.3/7.6.8), межпользовательский доступ к ключам через общую группуselfpost+setgid+RequireSafeKeys no, add/delete/список/TXT/подтверждение каскада/ручной reload, строгая валидация имени домена (7.6.2), injection-safe запись таблиц (7.6.4). Юнит-тесты + контейнерный e2e (add/delete, персистентность ключей через рестарт, reload) зелёные. Проверено на сервере. - Фаза 4 (2026-07-12, Opus) — приложения + SASL + привязка к домену: учётки в
sasldb2черезsaslpasswd2(пароль по stdin, логин whitelisted argv, без shell — 7.6.3), генерируемый пароль показывается один раз (7.6.1), режим адресов wildcard/list с серверной проверкой принадлежности адреса домену (7.6.2), генерацияsmtpd_sender_login_maps(many-to-one слияние, injection-safe — 7.6.4), CRUD приложений + перевыпуск пароля + каскад при удалении домена (очисткаsasldb2+ пересборка карты). Исправлен reload Postfix:signal HUPне доходит до форкнутого master → одноразовая supervisord-программаpostfix-reload(настоящийpostfix reloadот root без привилегий панели).postfixв группеselfpost,/data/sasl+/data/postfixпод setgid. Новые пакетыinternal/app,internal/postfix. Юнит-тесты + контейнерный e2e (весь жизненный цикл, каскад, персистентность, реальный reload поmail.log) зелёные.