feat(mail-admin): read-only account view for mail.produktor.io (#74)

- admin/: Go stdlib-only HTTP viewer, lists accounts from
  config/postfix-accounts.cf with per-mailbox message counts (INBOX and
  total, same numbers doveadm reports) and storage; quota from
  dovecot-quotas.cf; Basic Auth from .env; offline tests vs fixtures
  (go test -race ./...)
- compose: mail-admin service, build admin/Dockerfile, publishes
  127.0.0.1:19945 / 172.17.0.1:19945; config and mail-data mounted :ro,
  no docker socket
- NPM proxy host 66: location /admin/ -> 172.17.0.1:19945
- README: account admin section
This commit is contained in:
2026-09-01 15:07:55 +01:00
parent e00c6950ed
commit 5a46ba4b33
22 changed files with 790 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"os"
"strings"
"testing"
)
func TestParseAccounts(t *testing.T) {
f, err := os.Open("testdata/accounts.cf")
if err != nil {
t.Fatal(err)
}
defer f.Close()
accounts, err := parseAccounts(f)
if err != nil {
t.Fatal(err)
}
if len(accounts) != 5 {
t.Fatalf("got %d accounts, want 5", len(accounts))
}
want := []string{
"info@produktor.io",
"andriy.oblivantsev@produktor.io",
"postmaster@produktor.io",
"postman@produktor.io",
"ano@produktor.io",
}
for i, w := range want {
if accounts[i].Email != w {
t.Errorf("account %d = %q, want %q", i, accounts[i].Email, w)
}
}
if accounts[0].Hash == "" {
t.Error("expected a hash to be parsed")
}
}
func TestParseAccountsSkipsMalformed(t *testing.T) {
in := "# comment\n\nalice@example.org|hash1\nbroken-line-no-pipe\n|hash-no-email\n"
accounts, err := parseAccounts(strings.NewReader(in))
if err != nil {
t.Fatal(err)
}
if len(accounts) != 1 || accounts[0].Email != "alice@example.org" {
t.Fatalf("got %+v, want only alice@example.org", accounts)
}
}