release: 1.9.2: Review pass: port-25 milters, DMARC validation, rate-limit fix

Review pass over `1.4.0`…`1.9.1`: port 25 no longer shares the submission milter chain, DMARC reports are validated before they are stored, and the auto rate limit is correct for a non-default level-1 window. Schema migration `0010` (index only). Upgrading is a tag bump.
Added:
  - SQLite migration `0010_send_log_app_login_index.sql` —
  - DMARC aggregate reports are accepted as **zip** as well as gzip or plain XML
  - e2e: `checkPort25ConfigCombinations` regenerates the Postfix configuration
Changed:
  - README hero line and a **Positioning** block in `docs/product.md` record the
Fixed:
  - The setup account's password is hashed at bcrypt cost `12` like every other
  - Login/setup rate limiter: protecting blocked buckets from eviction is now
  - `postqueue -p` output is bounded to 4 MiB (the timeout alone left the size of
  - [schema-migrations.md](docs/schema-migrations.md): the schema section was
  - postqueue reader is now time-bounded: `postqueue -p` runs under a 5-second
  … (11 more in CHANGELOG)
Changed:
  - [roadmap.md](docs/roadmap.md) now carries everything the 2026-08-19 code
  - Extracted `internal/configsafe` — the empty-value and forbidden-character
  - Build, deploy and migration comments no longer cite the archived
  - Dropped the `parseDomainRateLimitForm` / `parseAppRateLimitForm` pass-through
  - Extracted shared helpers: `internal/atomicfile` (atomic temp-write+rename) and
  … (7 more in CHANGELOG)
Removed:
  - Unused legacy asset `docs/assets/icon.png` (superseded by the four brand SVGs
  - Plan documents for already-shipped features: `domain-stats-auto-ratelimit`,

Full detail: CHANGELOG.md (1.9.2).
This commit is contained in:
mix
2026-09-22 15:24:53 +03:00
parent 967d3d95de
commit 7ef7f75246
179 changed files with 11558 additions and 1738 deletions
+17 -21
View File
@@ -7,10 +7,12 @@ package postfix
import (
"fmt"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/mixeme/selfpost/internal/configsafe"
"github.com/mixeme/selfpost/internal/supervisor"
)
// Postfix manages the on-disk Postfix state the panel is responsible for. After
@@ -122,6 +124,15 @@ func appendUnique(list []string, v string) []string {
return append(list, v)
}
// What may never reach a sender-map line: whitespace and the newline that would
// end it, the comma that separates values, the colon a lookup key cannot carry,
// and the backslash. A login additionally may not carry "@" — that would make it
// look like an address.
const (
senderMapAddressForbidden = " \t\r\n,:\\"
senderMapLoginForbidden = " \t\r\n,:@\\"
)
// assertMapSafe rejects any address/login value that could break out of a single
// map line or inject a directive. Addresses are validated to a strict whitelist
// (letters, digits, '@', '.', '-', '_', '+') and logins to an even stricter one
@@ -129,14 +140,11 @@ func appendUnique(list []string, v string) []string {
// letting whitespace, a newline or a comma (the value separator) through into
// the file (security.md).
func assertMapSafe(address, login string) error {
if address == "" || login == "" {
return fmt.Errorf("postfix: empty address or login")
if err := configsafe.Token("address", address, senderMapAddressForbidden); err != nil {
return fmt.Errorf("postfix: %w", err)
}
if strings.ContainsAny(address, " \t\r\n,:\\") {
return fmt.Errorf("postfix: unsafe character in address %q", address)
}
if strings.ContainsAny(login, " \t\r\n,:@\\") {
return fmt.Errorf("postfix: unsafe character in login %q", login)
if err := configsafe.Token("login", login, senderMapLoginForbidden); err != nil {
return fmt.Errorf("postfix: %w", err)
}
return nil
}
@@ -154,17 +162,5 @@ func assertMapSafe(address, login string) error {
// Arguments are fixed literals — no user input is interpolated into the command,
// and it never goes through a shell (security.md).
func reloadViaSupervisor() error {
cmd := exec.Command("supervisorctl",
"-c", "/etc/supervisor/supervisord.conf",
"start", "postfix-reload")
out, err := cmd.CombinedOutput()
if err != nil {
// A reload already in flight is not a failure: that pending run reloads
// Postfix after our file is in place (the file is written before this).
if strings.Contains(string(out), "already started") {
return nil
}
return fmt.Errorf("reload postfix via supervisor: %w: %s", err, strings.TrimSpace(string(out)))
}
return nil
return supervisor.Start("postfix-reload")
}