Commit Graph
56 Commits
Author SHA1 Message Date
mixandClaude Opus 5.5 a1724ad72c release: 2.10.4: origin-strict - a change with neither header is refused
test / test (push) Waiting to run
Cut without asking, as the owner said on 2026-10-15. A POST with neither
Sec-Fetch-Site nor Origin is refused (the commit before this); the e2e
client sends Origin. go vet, go test (26 packages) and make e2e passed on
the test server for that tree. No migration, schema 12.

CHANGELOG [2.10.4]; pins in deploy/docker-compose.yml, README and the guide
(pin, build, the sample restore message); development's "from pin" and
row 36; roadmap row. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:16:28 +03:00
mixandClaude Opus 5.5 af778445d4 release: 2.10.3: backup-reauth - the account password before a backup
test / test (push) Waiting to run
Cut without asking, as the owner said on 2026-10-15. The full backup and a
domain's export ask for the signed-in user's account password under the
password change's limit; the import's file field is drawn as designed
(1888dce design, the code commit before this). go vet, go test (26
packages) and make e2e passed on the test server for that tree. No
migration, schema 12.

CHANGELOG [2.10.3]; pins in deploy/docker-compose.yml, README and the guide
(pin, build, the sample restore message); development's "from pin" and
row 35; roadmap row. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:39:14 +03:00
mixandClaude Opus 5.5 dd99e0f90b release: 2.10.2: phone-desktop-mode - a Menu with folded sections
test / test (push) Waiting to run
Cut without asking, as the owner said on 2026-10-15, after his choice of
2026-10-16 ("Вариант 1 ... сворачивание разделов"). Below 1024 px the
navigation is a Menu whose sections fold, the current one open; the page
has a gutter (e0e30e2 design, the code commit before this). go vet, go test
(26 packages) and make e2e passed on the test server for that tree. No
migration, schema 12.

CHANGELOG [2.10.2]; pins in deploy/docker-compose.yml, README and the guide
(pin, build, the sample restore message); development's "from pin" and
row 34; roadmap row. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:03:18 +03:00
mixandClaude Opus 5.5 1dc3b7e5fe release: 2.10.1: long, non-repeating queue ids
test / test (push) Waiting to run
Cut without asking, as the owner said on 2026-10-15, after his choice of
2026-10-16 ("1"): Postfix makes long queue ids (enable_long_queue_ids=yes),
so a later message can no longer meet an earlier one's rows. go vet, go test
(26 packages) and make e2e passed on the test server for that tree, long
ids on both an outbound and an inbound row. No migration, schema 12.

CHANGELOG [2.10.1]; pins in deploy/docker-compose.yml, README and the guide
(pin, build, the sample restore message); development's "from pin" and
row 33. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:59:19 +03:00
mixandClaude Opus 5.5 f4f6e0f23d release: 2.10.0: inbound-upstream-result - the upstream's answer in the Inbound log
test / test (push) Waiting to run
Cut at the owner's word (2026-10-16): "потом режь" after the check, which
he allowed ("Отправляй").

Inbound › Log says what became of accepted mail - queued, held, sent,
deferred, bounced - with the upstream's last reply, the host and when
(5348330, 295d72e, 46b90d2 design and plan; d515493 code, the reviews'
findings fixed in it). Migration 0012, schema 12.

On the test server as 2.9.1-post.d515493 (the same code), 2026-10-16:
healthy, every program RUNNING, keys unchanged, schema 12. One message
sent from the server to its own port 25 for selfpost@dev.mixdep.ru: its
row sent, "250 2.0.0 Ok: queued as 04F0D1023EF" from mc-ent.mixfed.ru. (The
owner's own message had gone to mc-ent directly: SelfPost is
dev.mixdep.ru's second MX.)

CHANGELOG [2.10.0]; pins in deploy/docker-compose.yml, README and the guide
(pin, build, the sample restore message); development's "from pin" and row
32; roadmap row and recommended order; plan closed. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:03:38 +03:00
mixandClaude Opus 5.5 6531db977e release: 2.9.1: log panes wrap on a phone
test / test (push) Waiting to run
Cut without asking, as the owner said on 2026-10-15, for his report of
2026-10-16 (two scroll bars over a message's Delivery log on his phone).

Log panes wrap their lines under their start instead of scrolling
sideways (216f366 design, bd21ff0 code): the Delivery log of a message,
System log, the queue's listing. go vet, go test (26 packages) and make
e2e passed on the test server for that tree. No migration, schema 11.
The inbound-upstream-result mockup and plan (5348330, 295d72e) ride
along; its code follows for 2.10.0.

CHANGELOG [2.9.1]; pins in deploy/docker-compose.yml, README and the
guide (pin, build, the sample restore message); development's "from pin"
and row 31. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:31:58 +03:00
mixandClaude Opus 5.5 446feadd74 release: 2.9.0: DNS records folded, Settings re-checks, /data/opendkim gone
test / test (push) Waiting to run
Cut at the owner's word (2026-10-16, "потом режь 2.9.0").

dns-record-collapse (fffee9c, 51f303c): a record DNS holds as shown is one
line, its values behind Show values. settings-save-row (f95c553,
82a53e1): the Settings form is the left column, Save under its fields.
notification-mail-recheck (98e4dfc, 52a9811, 3f5cd9a): Re-check DNS on
Settings, POST /server/settings/dns-recheck. And the release after 2.8.0
removes /data/opendkim (9420d8c): the entrypoint deletes the 2.7.x key
tree once /data/dkim holds a key, and stops on a /data that never ran
2.8.x.

Each tree passed go vet, go test (26 packages) and make e2e on the test
server; dns-record-collapse and settings-save-row ran there as
2.8.0-post.51f303c and 2.8.0-post.82a53e1. No migration, schema 11.

CHANGELOG [2.9.0]; pins in deploy/docker-compose.yml, README and the guide
(pin, build, the sample restore message; the 2.7.x rollback command keeps
naming 2.7.0); development's "from pin" and row 30; roadmap rows and
recommended order. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:46:26 +03:00
mixandClaude Opus 5.5 9420d8c03d entrypoint: /data/opendkim removed, as planned for the release after 2.8.0
2.8.x copied the keys from /data/opendkim to /data/dkim and kept the old
tree for a rollback to 2.7.x "until the next release"
(docs/plans/panel-dkim-signing.md, Q2); the owner's "потом режь 2.9.0"
(2026-10-16) makes this that release. The entrypoint now removes the tree
once /data/dkim holds a key, and says so. A /data with keys under the
old tree only - one that never ran 2.8.x - stops the start with a message
to run 2.8.x first, rather than let the panel give the server's own name
a new key its published record would not match; the tree is left as it
was. Gone with it: the copy into /data/dkim.new, the tree's
normalisation, its exemption from the general hand-over, its key link in
drop_links, and the backup's exclusion of "opendkim" (dead: the panel
never sees the tree now).

Tests: e2e TestStartsOnA28Data replaces TestStartsOnA27Data - a /data as
2.8.x left it (both trees, a queue of foreign uids, a rotation due): the
old tree is gone, the key under /data/dkim is the one left, the selector
table names it, the queue is Postfix's by name; TestRefusesA27Data - the
start stops, says why, leaves the old key and makes no new one. go vet +
go test (26 packages) and make e2e (all pass) on the test server for this
tree.

Docs: guide § Upgrading "To 2.9.0" (rollback to 2.8.x needs nothing, to
2.7.x only from a 2.7.x backup; the 2.7.x rollback paragraph says it is
for 2.8.x only and keeps naming 2.7.0), guide § backup, architecture
(the old tree, § Persistence's table and the restore line), security
§ Backup, development's e2e paragraph, the plan's key step; CHANGELOG
[Unreleased] § Removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:46:04 +03:00
mixandClaude Opus 5.5 ac21180d58 release: 2.8.0: panel-dkim-signing - the panel signs, OpenDKIM gone
test / test (push) Waiting to run
Cut at the owner's word (2026-10-16, "Выпускай").

The panel signs outgoing mail in a milter of its own (go-msgauth,
relaxed/simple, the login's key on 465/587 and the server's name's on
local mail); OpenDKIM, the syslog relay, the USR1 reload and the From:
parser port are gone. Keys move to /data/dkim (copied once; /data/opendkim
kept for a rollback, left out of backups). The sender check holds Sender:
like From:. go-milter is built patched not to keep every header. The queue
is handed to Postfix's users by name before "postfix check". Commits
005922b..ca3a183 (005922b, faa3c11, c2f63d2, f0dd9f9, 2cfd239, d9d9af3,
0cceb54, 80faf1a, f1d90a2, ca3a183).

On the test server since 2026-10-15 22:48 UTC (2.7.0-post.c2f63d2, now
2.7.0-post.f1d90a2, the same code as this cut): healthy, keys unchanged.
The first rollout (faa3c11) crash-looped five minutes on the queue's owner
numbers and was rolled back; fixed in c2f63d2. The owner's canary: Gmail
dkim/spf/dmarc pass with d=carrel.mixfed.ru, Yandex dkim/spf pass, not
spam; his From:/Sender: check, thirteen cases, all as expected.

CHANGELOG [2.8.0]; pins in deploy/docker-compose.yml, README and the guide
(the guide's rollback command keeps naming 2.7.0 on purpose); development's
"from pin"; roadmap row and recommended order; plan stage 3 closed. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:36:54 +03:00
mixandClaude Opus 5.5 f1d90a2a1b go-milter: build it patched not to keep every header
test / test (push) Canceled after 0s
The security documentation's last open item of panel-dkim-signing (the
owner's decision, 2026-10-16): go-milter v0.4.1 added every header of a
session to a map of its own, emptied only on ABORT, and handed it to
Headers - which no milter of the panel reads. Postfix bounds one header,
not their number, so a message of nothing but headers up to the message
size limit was held whole by the library in the sender check, the signer
and the journal milter on 465/587 and in the inbound journal milter on port
25, where strangers send; the signer's 1 MiB refusal came after it was all
in.

third_party/go-milter is v0.4.1 from the module cache (BSD 2-Clause,
LICENSE kept) with session.go changed: CodeHeader hands the header to
Header and keeps nothing, CodeEOH hands Headers an empty map. PATCHES.md
says what and why, and how to take a new upstream version. go.mod replaces
the module with it; the Dockerfile copies third_party before "go mod
download". TestTheLibraryKeepsNoHeaders holds it through the library's own
client over two messages of one connection. Comments, security.md (the
accepted risk becomes a note), development.md's library table, the plan and
CHANGELOG follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:19:57 +03:00
mixandClaude Opus 5.5 2cfd239d16 docs: panel-dkim-signing stage 3 - the documents without OpenDKIM
test / test (push) Canceled after 0s
architecture § OpenDKIM becomes § DKIM signing (keys, the selector table
and when it is rewritten, the two sockets, what is signed and the limits,
the old tree); the process table, start order, liveness, the mail-path
diagram, § Sender check, Persistence and the restore Resync follow the
code. security.md: the panel signs with the login's key, the keys are the
panel's alone, group selfpost and CAP_KILL as they are now; OpenDKIM's
accepted risks go; new ones: what sendmail(1) submits is signed as the
server, Sender: is signed but not judged (the owner's decision), and
go-milter keeps every header per session, so the signer's 1 MiB refusal
does not bound the library's copy. guide: Health, the container log's
dkim-signer lines, backups, the long-line limit, and upgrading to 2.8.0 -
with the rollback to 2.7.x: its start reads the queue as its own postfix
(uid 101) before it hands the queue over, so the hand-over by name is run
in the 2.7.x image first (measured on a copy of the test server's queue:
postsuper failed before, passed after). development, README, SECURITY,
product, the assumptions register's header: what left the image.
entrypoint.sh: a comment, the rollback note as the guide gives it. Plan
stage 3 lines ticked, roadmap 12/14.

Written by four agents, one document each, against a brief of what the
code does; reviewed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 01:56:56 +03:00
mixandClaude Opus 5.5 faa3c11ae7 panel-dkim-signing stage 2: OpenDKIM removed
test / test (push) Canceled after 0s
The panel's signer is the only one. Gone: the opendkim package and its
configuration, the syslog-relay and opendkim programs, /dev/log and
/run/opendkim, the reload by USR1 through supervisord
(internal/supervisor/supervisor.go), internal/syslogrelay, the port of
OpenDKIM's From: parser (internal/milter/mailparse.go and its probe,
harness and test/upstream scripts), OPENDKIM_SOCKET and OPENDKIM_DIR.

Keys: internal/domain/keys.go (Keys, NewKeys, DKIM_DIR=/data/dkim) keeps
the keys and writes a selector table, /data/dkim/selectors ("<name>
<selector>"), which the signer reads in place of the KeyTable - it reads
no database, as the sender check reads none. The table is written before
the signer serves (EnsureServerSigned moved there from the notifier).
Keys and table 0600, directories 0700, the panel's. The entrypoint copies
/data/opendkim/keys once, as the panel user, into /data/dkim.new renamed
when whole, and stops the start if the copy fails; /data/opendkim is left
for a rollback to 2.7.x.

Sender check: one RFC 5322 address as a mail program reads it, held
against the bindings; the 256-byte refusal and the texts naming OpenDKIM
go. Allowed() folded the domain through strings.ToLower, which makes the
Kelvin sign a "k" - the comparison with OpenDKIM's reading had hidden it;
it folds ASCII only now.

Health: two dkim-signer rows (applications, notifications) in Milter
sockets, opendkim out of the liveness set; the card no longer takes any
row with a note for the spam filter. Texts as in the design commit.

KILL stays in deploy/docker-compose.yml: measured on the test server,
root without it cannot signal a process of another uid - supervisord
stopping the panel, Postfix's master its children. The plan's "only use
was the reload" was wrong; the comment now gives the real uses.

e2e: one signature, the panel's, verified by dkimpy; the /data/dkim modes;
the OpenDKIM log and process checks, testNeverSignedForAnother and the
cited opendkim, libopendkim11 and libmilter1.0.1 go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 01:05:39 +03:00
mixandClaude Opus 5.5 a556a13f67 release: 2.7.0: base-trixie - Debian trixie, Postfix 3.10.13
Cut without asking, as the owner said on 2026-10-15.

The image runs debian:trixie-slim with Postfix 3.10.13, libmilter
8.18.1, OpenSSL 3.5; every citation re-read against the trixie sources.
smtpd_hide_client_session=yes on 465/587 (owner's decision). The
selfpost group is 10998 and the panel 10999:10999, outside the system
range; the entrypoint re-owns /data by name. The panel's database is
0600; the entrypoint removes symbolic links the panel could leave under
/data. Commits 68ec8b2, 1c6e04b, a3050af.

Rolled out to the test server as 2.6.1-post.a3050af (the same code),
2026-10-15 17:07 UTC: healthy, every program RUNNING, no restarts, three
messages sent, the queue empty, no warning but outside password guessing.
The canary: one message from an application through 465 to Gmail, Yandex
and the test mailbox - Received: in the short form everywhere; Gmail
dkim=pass spf=pass dmarc=pass; Yandex spf=pass dkim=pass, not spam.
No migration, schema 11.

CHANGELOG cut; pin 2.7.0 in deploy/docker-compose.yml, README, guide (and
its sample restore message); development.md "from pin" and the rows of
base-trixie and panel-dkim-signing; roadmap row and recommended order.
No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:14:14 +03:00
mixandClaude Opus 5.5 1c6e04b7d3 base-trixie: the image on Debian trixie, Postfix 3.10.13; Received: hidden on 465/587
test / test (push) Canceled after 0s
The runtime base is debian:trixie-slim, the build stage golang:1.26-trixie:
Postfix 3.10.13 with upstream's 2026 security fixes up to the August
batch, libmilter 8.18.1, OpenSSL 3.5. Every citation of another program in
code, scripts and documents was re-read against the trixie sources
(dev/upstream; bookworm's kept in dev/upstream-bookworm) and updated;
development.md's cited-package table holds the trixie versions.

What the re-reading changed in behaviour:
- logtail: reject lines with sasl_method/sasl_username/sasl_sender
  (smtpd_check.c:1019-1026); cleanup's "removed (canceled)" forgets a
  port-25 id (cleanup_api.c:363-364).
- internal/postfix/retry.go reads postconf's stdout only: since 3.9 a
  deprecated-parameter warning on stderr made one value too many and the
  panel fell back to the compiled-in retry policy.
- logrotate-loop.sh: logrotate 3.22 exits 1 when it ignores a config;
  the loop refuses on that output whatever the status.
- postfix-config.sh: cyrus_sasl_config_path set explicitly (bookworm's
  07_sasl_config.diff is gone); maillog_file_permissions=0640; mynetworks
  explicit; Dockerfile: SYNC_CHROOT= in /etc/default/postfix,
  opendkim-tools dropped, openssl named, systemd-standalone-sysusers so
  cron does not bring the whole of systemd.
- The selfpost group is 10998, outside the system range 100-999: in
  trixie a package took 998 and the build failed (owner's decision:
  a conflict-free id, the distribution's own ids untouched). The
  entrypoint hands /data back by name when the user or the group differs.
  Guide, backup refusal text, help (design commit 68ec8b2), e2e.
- smtpd_hide_client_session=yes on smtps and submission (owner's
  decision): Received: names no client, TLS, login or protocol; port 25
  keeps the full header. e2e checks both.
- test/upstream/opendkim-probe.sh makes keys with openssl genrsa.

Tests: on the server, make e2e exit 0 (674 s, the image built on trixie),
go vet + go test 27 packages ok. CHANGELOG [Unreleased], guide,
architecture.md, security.md, roadmap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:31:35 +03:00
mixandClaude Opus 5.5 b76f1ef1a0 release: 2.6.1: port 25 ends DATA only at CRLF.CRLF
test / test (push) Canceled after 0s
Cut without asking, as the owner said on 2026-10-15.

The inbound relay's smtp/inet listener sets
smtpd_forbid_bare_newline=normalize, the SMTP-smuggling defence Postfix
3.7.11 leaves off (aaa9412, port25-bare-newline, the first of the three
rows from own-mta-builds).

Rolled out to the test server as 2.6.0-post.aaa9412 (the same code): the
container healthy, every program RUNNING; the server runs the inbound
relay, and postconf -P shows smtp/inet/smtpd_forbid_bare_newline =
normalize next to syslog_name postfix/inbound; no warning or fatal in
docker logs. No migration, schema 11.

CHANGELOG cut; pin 2.6.1 in deploy/docker-compose.yml, README, guide (and
its sample restore message); development.md "from pin" and the row;
roadmap row shipped and the recommended order. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:30:33 +03:00
mixandClaude Opus 5.5 6023feb405 audit 6: documents, comments, dead code and staticcheck; the test gaps closed
test / test (push) Canceled after 0s
The last stage of the 2026-10-12 audit; no cut.

Doc drift: architecture.md (the sender check in § OpenDKIM, the limit:
row's 48 hours, the inbound log and filter-list routes and who may open
them, migrations to 0011 and every table, KitPage as production data,
Reload configuration as a restore writes, the panel's roles counted),
schema-migrations.md (tables and columns since v2), guide.md (a delivery's
lines go at the next prune; INBOUND_JOURNAL_MILTER_SOCKET moved to
architecture.md's internal list), development.md (the env guard, the
deferral the e2e proves, its fixed sleeps), roadmap.md.

Comments say what the code does now; TestBaselineIsTheOnlyMigration is
TestChainStartsAtTheBaseline; the milter package doc names its three
milters.

Dead code gone: view.Facts, dmarc HostedSuggestion and
ValidateHostedAddress, app UpdateMode (each with the test that alone kept
it), the method checks the method-bound routes made dead;
Settings.WithResult calls resultFlash. staticcheck 2026.1 in test.yml on
both modules, clean: the e2e package builds without its tests now (a test
and two constants moved out of non-test files), three findings fixed.
The test moved, TestImageBuildPreservesLogrotateMode, had sat in a
non-test file and never run; run, it failed on the entrypoint (docker run
without a hostname) and now asks with --entrypoint stat.

The test gaps no step named, each closed: follow() with mail.log gone past
blindMin and made again; CheckRestore with a manifest that is not JSON;
the origin check with a port; every /server/ pattern in web.go through
server() and in the routes table (it found GET /server/health/fragment
missing from the table - guarded, not tested); L9 as the owner decided it
on 2026-10-12, the comment on senderSession.Headers and the test passing
an h that holds the earlier messages' From:.

Plan stage 6 ticked; roadmap audit row done, 81/81.

Tested on the test server: go vet + go test of 27 packages, make e2e (all
pass, 513 s) on this tree; staticcheck locally on both modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:41:26 +03:00
mixandClaude Opus 5.5 dc78b0ed76 release: 2.6.0: audit stage 5 - image and Postfix hardening
test / test (push) Canceled after 0s
Cut without asking, as the owner said on 2026-10-15.

The supervisord socket is root:supervisor, the panel the group's one member
(M17). Postfix takes TLS 1.2 at least, the client too where TLS is
required; VRFY off; the banner without the distribution; the start refuses
a rate, size or milter timeout that is not a number, under its minimum or
past an int (L31). OpenDKIM logs through the syslog relay to the container
log and oversigns From: (L30). crashexit takes the container down after
five unexpected exits of a mail-path program in ten minutes (L32).
logrotate keeps its state under /data/log. extract-cert.sh refuses what is
not PEM and renames whole files (M21). Plus the findings of both reviews
(a39eda5). Commits dab7715, a39eda5, b09e525.

Rolled out to the test server as 2.5.4-post.95af363 (the same code): the
container healthy, every program RUNNING, syslog-relay among them;
OpenDKIM's start line in docker logs under "syslog: "; the postconf values
as set; /run/supervisor.sock root:supervisor 0770 and the panel reaches it;
/data/log/.logrotate.status written. No migration, schema 11.

CHANGELOG cut; pin 2.6.0 in deploy/docker-compose.yml, README, guide (and
its sample restore message); development.md "from pin" and the stage's row;
roadmap row and recommended order; stage 5 marked shipped in the plan. No
tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:58:10 +03:00
mixandClaude Opus 5.5 d6ed6bcb30 release: 2.5.4: audit stage 4 - panel behaviour
test / test (push) Canceled after 0s
Cut without asking, as the owner said on 2026-10-15.

Signing in costs the same for an unknown name (M8) and ends the session the
browser presented (L16); the Account password check has its own limit
(L15). The HTTP server bounds the request, the answer and idle connections;
the full backup moves its own deadline (L14). Monitoring refreshes are
scheduled from htmx:afterRequest with a back-off and a 30 s timeout, and a
lost session sends htmx to sign-in (M11). The Outbound log clamps its page
(L10) and filters by status and day; the multiplier refuses NaN (L11); a
rate-limited refusal is red (L18); Recalculate asks before it drops an
unsaved edit (L19); Reload configuration writes what a restore writes and
removes the keys of deleted domains (L13, L17). The cards layout orders by
number. A notification is owed to each recipient (migration 0011). A DNS
outage is unknown, each resolver with its own time (M14, M15). DMARC
reports too large are refused for their size. An inbound upstream has one
TLS mode, changed for all its domains (L5). Plus the findings of both
reviews (eaa7de0). Commits 07be0cf, 643892a, ca876c1, 80e3979, 2b8e0c0,
b90a2f0, eee089a, 006bed7, 32dfe4a, eaa7de0.

Rolled out to the test server as 2.5.3-post.eaa7de0 (the same code):
migration 0011 applied, schema 11; all programs RUNNING, the container
healthy. Resync's key cleanup checked against the live data: every key
directory is a domain's or the hostname's, nothing to remove.

CHANGELOG cut; pin 2.5.4 in deploy/docker-compose.yml, README, guide (and
its sample restore message); development.md "from pin" and the stage's row;
roadmap row and recommended order; stage 4 marked shipped in the plan. No
tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:51:50 +03:00
mixandClaude Opus 5.5 48832d223a release: 2.5.3: audit stage 3 - persistence and restore
test / test (push) Canceled after 0s
Cut without asking, as the owner said on 2026-10-15: "Вопросы про резку не
задавай, просто режь."

A restore is ended by its Resync: the manifest stays until the first start
has regenerated the maps (M1). Every archive ends with data/contents.json and
the first start after a restore compares /data with it before it opens the
database: half an archive, a damaged file, a journal left beside the
database is refused, named. The entrypoint hands every map under
/data/postfix to the panel (a restore unpacked as root left the DMARC maps
unreadable - found by the review). Migration 0010: the send log's sweep,
refusal-line check, level-2 count and pages each by its index, pinned by
EXPLAIN tests on the statements the store runs (M2). The backup CLI refuses
a missing database (M3) and a password under 12 (M5), writes -o whole,
checks -decrypt's password before the output (L1), warns of what it leaves
out and refuses an empty certs/ (L2). An encrypted file may ask scrypt for
128 MiB at most, r <= 64, p <= 2, and nothing may follow its last chunk
(M4, L3). TRUSTED_PROXY_CIDR's bare ::ffff:a.b.c.d is one host (L4). Maps
are durable across a power loss: the directory is synced (L7). Commits
8e3241e, 508ac96, 242515c, 76b21b3.

Rolled out to the test server as 2.5.2-post.76b21b3 (the same code):
migration 0010 applied, schema 10, the four new indexes present; all
programs RUNNING, Postfix with its three milters, nothing in the logs. On
the live container: a CLI backup ends with data/contents.json, a wrong
SELFPOST_DB_PATH is refused and creates nothing, a short password is
refused.

CHANGELOG cut; pin 2.5.3 in deploy/docker-compose.yml, README, guide (and
its sample restore message); development.md "from pin" and the stage's row;
roadmap row and recommended order; stage 3 marked shipped in the plan. No
tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:44:08 +03:00
mixandClaude Opus 5.5 68b3a7b23f release: 2.5.2: audit stage 2c - what SelfPost relies on, read in the sources
test / test (push) Canceled after 0s
The owner's word, 2026-10-15: "Да, режем."

Every place SelfPost relies on another program was read in that program's
source, of the version the image runs, and is cited in the code (621
entries, docs/assumptions-2026-10-14.md); what the sources contradicted is
fixed: rspamd signing a sender's own verdict, the sender map's first key,
queue expiry and Postfix's time values, refusal and recipient lines as
Postfix writes them, the rotated log read until postlogd lets it go,
inbound mail over 10 MB, local delivery to an address literal, supervision
(Postfix stopped, give-ups FATAL, the reload's exit read), sasldb2 through
a copy, Health dialling sockets and showing the certificate Postfix
serves, Referrer-Policy same-origin, migrations under BEGIN IMMEDIATE,
certbot's links followed, about forty sentences of the documents, and ten
measurements on the stand. The sender check reads From: with OpenDKIM's
own parser, ported and checked against the C and against OpenDKIM's
signing; L8 is closed; an address OpenDKIM would cut short is refused.
Plus the findings of both reviews of the stage (9eddd49).

Rolled out to the test server as 2.5.1-post.9eddd49 (the same code): no
migration, schema 9; all programs RUNNING, the container healthy,
Postfix with its three milters, nothing in the logs.

CHANGELOG cut; pin 2.5.2 in deploy/docker-compose.yml, README, guide (and
its sample restore message); development.md "from pin"; roadmap row and
recommended order; stage 2c marked shipped in the plan. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:22:18 +03:00
mixandClaude Opus 5.5 fbd0acd035 audit 2c: the rest the sources contradict, and the documents
test / test (push) Canceled after 0s
Code and config:
- C-M7: a DMARC report's explicit <pct>0</pct> stays 0; only an absent
  pct takes RFC 7489 § 6.3's default 100 (Pct *int).
- C-D1: docs/development.md now tables the Debian versions the citations
  were read at (postfix 3.7.11-0+deb12u1, opendkim and libopendkim11
  2.11.0~beta2-8+deb12u1, libmilter1.0.1 8.17.1.9-2+deb12u2, libsasl2-2 and
  sasl2-bin 2.1.28+dfsg-10, supervisor 4.2.5-1, logrotate 3.21.0-1 - read off
  the image with dpkg-query, 2026-10-15); the e2e stand's new
  checkCitedPackageVersions fails when the image it built differs.
- C-E2: the e2e zone's SOA serial only grows (CoreDNS v1.14.6's file
  plugin reloads on a grown serial, not on mtime); Corefile comment says
  so; TestZoneSerialGrows.
- C-K8, C-K9: the e2e compose and stage comments say how
  --project-directory and .env resolve.
- D-arch:154: postfix-config.sh's "Postfix insists" comment says what
  TLS_README advises and that smtpd reads the key as root.
- B-G7: inflight.go's reservationTTL comment: a dropped client does give
  ABORT (smtpd_proto() -> mail_reset() -> milter_abort()); the TTL is for
  smtpd dying or the milter connection breaking.
- D-guide:301: the nginx fragment's first-certificate command now reaches
  certbot (--entrypoint certbot, --standalone, port 80 free), and the
  renewal loop passes --webroot -w /var/www/certbot - a standalone
  certificate would otherwise renew standalone while nginx holds port 80
  (certbot docs, "Renewing certificates": the issuing plugin is used
  "unless you specify other plugins or options", and options of a
  successful renewal are saved). The register's proposal missed that.

Documents: every D-arch, D-guide and D-sec claim the register's Answers E
found false or partly true, rewritten - by three Sonnet agents, one per
document, then reviewed: two proposals were obsolete after this stage's
fixes and not used (a bounce's From: line - extract_from = "smtp" now;
"Origin: null" - Referrer-Policy is same-origin now); a stand
observation the agent had dated from a commit is now tied to the e2e test
that checks it; the SQLite writers paragraph describes _txlock=immediate;
the log tailer's rotation paragraph describes the minute's grace; the
guide's nginx steps were put in working order (links and modes before
`up`, no reconfigure step). "Deferred" is kept for Postfix's queue in the
guide too. guide.md's CRLF from the agent normalised.

Stand (2026-10-15, make e2e, exit 0, the version gate and zone serial
test included); go vet and go test ./... pass on the server (24 packages).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:53:04 +03:00
mixandClaude Opus 5.5 811055d4c0 development: the rule's example cites the function Postfix really has
The example comment named check_sender_login() and strcasecmp; Postfix
3.7.11 compares the login in reject_auth_sender_login_mismatch() with
strcasecmp_utf8 (src/smtpd/smtpd_check.c:3913, 3936). Found by the
heuristics inventory - the rule's own example was written from memory.
Says where the Debian sources are fetched from and kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:58:04 +03:00
mixandClaude Opus 5.5 0f984ff8d8 development: code relying on another component cites its source; stage 2c: the heuristics review
test / test (push) Canceled after 0s
Owner, 2026-10-14: "нам нужно полное ревью кода на предмет всей этой
эвристики. Её не должно там, где можно посмотреть код и ответить точно и
однозначно", and "в коде таких наших блоков должно быть обоснование со
ссылкой на код компонента".

- development.md § Code that relies on another component's behaviour: the
  rule, with the form of the comment (component, the image's version, file,
  function; or a document's URL and section; or "measured on the stand,
  date" said as such), and fixtures saying whether they were seen or
  composed.
- Audit stage 2c: the line "what was taken on trust" becomes the heuristics
  review - an inventory over the whole tree written down as
  docs/assumptions-2026-10-14.md, then each answered from the source of the
  version the image runs, the comment added, the code fixed where it
  contradicts. Plan still 66 lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:44:19 +03:00
mixandClaude Opus 5.5 3c8628074f release: 2.5.1: audit stage 2b - the log tailer and what the panel shows of a refusal
test / test (push) Canceled after 0s
A log rotated while the panel was down is read to its end; the deferred
counter remembers a place in the log, not a second; a refused message's
page shows what Postfix wrote; a refusal the panel did not get to write is
written from mail.log; downtime is estimated (≈) when supervisord is
silent; the Sender check card counts connections and messages apart (the
owner's decision); a message the sweep closed says the panel closed it.
Plus the findings of both reviews of the stage.

Rolled out to the test server as 2.5.0-post.e1ddf12 (the same code):
migration 0009 ran on the live database - schema 9, the new columns in
place, 68/68 send-log rows still tied to their domain, integrity ok.

CHANGELOG cut; pin 2.5.1 in deploy/docker-compose.yml, README (with the
local-build note), guide; development.md "from pin"; roadmap row and the
recommended order; stage 2b marked shipped in the plan. No tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:43:20 +03:00
mixandClaude Opus 5.5 5dcff8c2b9 audit: stage 2c - the sender check reads From: as OpenDKIM does (owner's decision)
test / test (push) Canceled after 0s
Owner, 2026-10-14, after reading OpenDKIM's source together
(dkim_mail_parse, called in mlfi_eoh on the first From:; SenderHeaders is
unset in build/opendkim.conf): "Да" to porting the parser so that the check
decides by the address OpenDKIM will take. The scan of the display name and
comments for another sender of the server - a guard against two parsers
parting ways, and the reason L8 is half closed - then goes.

New stage 2c (three lines) after 2.5.1 and before stage 3: the port from
the version the image runs, checked against OpenDKIM on the stand with
generated lines, then docs and wording. Stages 3 and 4 move to 2.5.3 and
2.5.4 (development.md's planned cuts). Plan 65 lines, roadmap 30/65.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:19:34 +03:00
mixandClaude Opus 5.5 5be109b871 release: 2.5.0: audit stages 1 and 2 - the delivery log and the tenant boundary
test / test (push) Canceled after 0s
Stage 1: a cut queue listing no longer bounces held mail; the reconcile
sweep closes only queued rows, by id, marked "reconcile", and its fate is
measured (delivery_log_days, migration 0007); delivery lines are read at
the head of the line; a renewed TLS certificate is served without a
restart; Health counts the message a hung panel deferred.
Stage 2: logins folded to lower case and unique without regard to case;
the last global administrator kept inside the transaction, users saved
whole; a domain import held to the forms' rules; log rows by domain id
(migration 0008); the 550 reply names no other domain; which key OpenDKIM
signs with, tested. Plus the findings of both reviews of each stage.

Rolled out to the test server as 2.4.2-post.ef18ef2 (the same code):
migrations 0007 and 0008 ran on the live database - schema 8, 68/68 send-log
and 10/10 inbound rows tied to their domain, integrity ok.

CHANGELOG cut; pin 2.5.0 in deploy/docker-compose.yml, README (with the
local-build note), guide; development.md "from pin"; roadmap row and the
recommended order; stages 1 and 2 marked shipped in the plan. No tag (the
owner's command only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:22:04 +03:00
mixandClaude Opus 5.5 9efc9c7005 release: 2.4.2: audit stage 0, release hygiene
test / test (push) Canceled after 0s
Stage 0 of docs/audit-2026-10-12.md, 7/7: the release image says which
release it is (H1), the e2e module is compiled on every push (M20),
test/ is out of the build context (L33), the documents say the 2.x image
is not published and show the local build (M6), dependencies raised,
staticcheck clean, the negative e2e cases held to their own refusals
(which showed the level-1 rate-limit case had never seen the limit).

CHANGELOG cut; pin 2.4.2 in deploy/docker-compose.yml, README (with the
local-build note), guide; development.md "from pin" and the planned cuts
of the audit's stages (2.5.0 ... 2.6.0); roadmap row in progress and the
recommended order; stage 0 marked shipped in the plan. No tag (the
owner's command only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:11:25 +03:00
mixandClaude Opus 5.5 80bab92c8c deps: modernc.org/sqlite 1.60.1, x/crypto 0.58.0, x/sys 0.49.0 (audit stage 0)
- go get ...@latest + go mod tidy: sqlite 1.53.0 -> 1.60.1 (libc
  1.77.1, memory 1.12.1), x/crypto 0.54.0 -> 0.58.0, x/sys 0.47.0 ->
  0.49.0, go-isatty 0.0.24. The go line reads 1.26.0 (tidy's spelling of
  1.26); no dependency asks for a newer Go.
- development.md: golang:1.26-bookworm floats to the newest patch, and
  the standard library's security fixes arrive that way - rebuild the image
  after each Go patch release; dependencies are raised by hand.
- CHANGELOG [Unreleased], audit plan line ticked, roadmap 5/61.

Evidence on the test server (Go 1.26.5) for this tree plus the next
commit's staticcheck fixes: go vet clean, go test ./... 23 packages ok,
make e2e exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:55:12 +03:00
mixandClaude Opus 5.5 937b41ddb7 docs: say the 2.x image is not published; show the local build (audit M6)
README, guide and deploy/docker-compose.yml pinned
ghcr.io/mixeme/selfpost:2.4.1, which does not exist: ghcr.io holds 1.0.0
and 1.3.0 only. The owner decided (2026-10-12) that old tags and images
stay untouched and the pin points forward, made true by the next
publication. Until then:

- README Quick start and guide Fixed image tag say the 2.x line is not on
  ghcr.io and show `docker build ... --build-arg VERSION=2.4.1 -t
  ghcr.io/mixeme/selfpost:2.4.1 .`; with the image present locally,
  docker run and Compose (pull_policy missing) use it without pulling.
- compose header says the same in two lines.
- development.md release steps: bump the version in those notes at each
  cut, remove them at the first publication of the line.
- CHANGELOG [Unreleased], audit plan line ticked, roadmap 4/61.

Documents only; no test reads the pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:42:53 +03:00
mixandClaude Opus 5.5 cc2ca3c968 ci: compile the e2e on every push; test/ out of the build context (audit M20, L33)
test / test (push) Canceled after 0s
- test.yml: after go test, `go vet ./...` and `go test -c -o /dev/null .`
  in test/e2e - compiled and linked, not run. Not `go test -run xxx` as
  the plan said: TestMain would still run and bring up the stand. The Go
  cache now keys on test/e2e/go.sum too.
- .dockerignore: test (e2e leftovers in test/e2e/.stage - DKIM keys,
  sasldb2 - no longer reach the builder; the sink image is built from
  test/e2e/sink as its own context, so it is unaffected).
- development.md (test.yml line), CHANGELOG [Unreleased], audit plan
  lines ticked, roadmap 3/61.

Evidence on the test server for this tree: make e2e (no
SELFPOST_E2E_VERSION, so the "e2e" default path) exit 0, including
image_stamped_with_version and the logrotate-mode build; in test/e2e
go vet ./... and go test -c -o /dev/null . clean; docker build of
build/Dockerfile with a file under test/e2e/.stage succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:24:48 +03:00
mixandClaude Opus 5.5 70b4eb5585 release: stamp the image with the release's version (audit H1)
test / test (push) Canceled after 0s
The e2e gate rebuilt selfpost:e2e through compose.override.yml with its
own VERSION=e2e, and that rebuild is the image release.yml pushed: every
published image printed "e2e", and the backup/restore version check
compared a backup's version with "e2e".

- compose.override.yml: VERSION: ${SELFPOST_E2E_VERSION:-e2e}.
- release.yml: the e2e step gets SELFPOST_E2E_VERSION = the release's
  X.Y.Z; a new step before the push checks panel -version and
  selfpost-backup -version of selfpost:e2e against it.
- e2e: image_stamped_with_version checks both binaries against
  expectedVersion() (the variable, or "e2e" when unset).
- development.md, CHANGELOG [Unreleased], audit plan line ticked,
  roadmap 1/61.

Evidence: make e2e on the test server for this tree with
SELFPOST_E2E_VERSION=2.4.2-stamp - exit 0, TestE2E/image_stamped_with_version
PASS, and `docker run --rm --entrypoint <bin> selfpost:e2e -version` prints
2.4.2-stamp for panel and selfpost-backup. GOOS=linux go vet in test/e2e
clean. Published images are left as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:18:20 +03:00
mixandClaude Fable 5.1 2a96008803 release: 2.4.1: code audit of 2.4.0 and its plan
test / test (push) Canceled after 0s
Docs only. docs/audit-2026-10-12.md: findings of a full-tree audit (four
high, twenty-one medium, the rest), doc drift, test gaps, what was checked
and found sound, and a six-stage plan; roadmap index row; CHANGELOG cut and
pin bump. Old tags and images stay as they are (owner): fixes go forward.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-12 22:57:08 +03:00
mixandClaude Opus 5.5 8f226bd82f release: 2.4.0: an application signs only as itself
test / test (push) Canceled after 0s
from-header-binding (docs/plans/from-header-binding.md, 9/9), at the owner's
word (2026-10-12, "Да").

  - A message's From: line is held to what its envelope sender is held to:
    any address of the application's domain, or the addresses of its list,
    with or without a +extension. Anything else is refused with 550 5.7.1 -
    another registered domain, the server's own name, a domain this server
    does not sign for. Until now an application could have mail signed with
    another domain's key by writing that domain into From:.
  - The check is the panel's, first in the milter chain and strict: while
    the panel does not answer, Postfix defers mail.
  - Server > Health counts what that costs - the panel's downtime under a
    running Postfix and the submissions turned away - and is laid out anew,
    boxes numbered by importance.
  - A refused row of the Outbound log says why in its tag.
  - Migration 0006.

Upgrading: mail whose From: is not the application's own stops being
accepted - another of your domains, an address off an application's list, a
visitor's address a form puts there. The guide says what to do about each.
A stopped panel now stops submission.

Not done, written into the plan at the owner's word: the Delivery log box on
a refused message's page, the log's status filter, "messages" that are
connections, and what was reasoned about rather than run - OpenDKIM's own
parser above all.

Cut: CHANGELOG 2.4.0 written (nothing had been put under [Unreleased]
while building); image pin 2.4.0 in compose, README, guide; version table,
roadmap, plan status. security.md: a malformed packet on a milter socket,
as an accepted risk. No tag: the owner tags when he decides to, so there is
no image of this version until then.

On the test server, on c817f08: go vet ./... ok, go test ./... ok
(23 packages), make e2e ok. This commit changes documents and the pin only.

Full detail: CHANGELOG.md (2.4.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:00:29 +03:00
mixandClaude Opus 5.5 ce7b09a65c release: 2.3.0: the panel writes to its users
test / test (push) Canceled after 0s
panel-notifications (docs/plans/panel-notifications.md, 10/10), at the
owner's word (2026-10-12, "Да").

  - Four things are reported by e-mail, each once: a sending domain's
    DNS record that does not match, a level-2 rate limit refusing mail,
    the TLS certificate about to expire, a message more than three days
    in the queue. Global users are told all of it, domain users about
    their own domains.
  - Sent from postmaster@<SELFPOST_HOSTNAME> and signed with a DKIM key
    of the server's own name, which the panel generates.
  - Account has the user's switch, Server > Settings the instance's and
    the two DNS records to publish, Server > Health their state.
  - An account e-mail may now be at a public mail host.
  - Migrations 0004 and 0005.

Upgrading: publish the two records Server > Settings shows for the
hostname (a DKIM key and "v=spf1 a -all"); until then notifications are
sent and may be filed as spam. Every user with an e-mail starts with
notifications on. non_smtpd_milters now names OpenDKIM.

Accepted and recorded in security.md: an application can have a message
signed as the server's own name by writing it into From: (roadmap
candidate from-header-binding).

Cut: CHANGELOG [Unreleased] -> [2.3.0]; image pin 2.3.0 in compose,
README, guide; version table, roadmap, plan status. No tag: the owner
tags when he decides to (2026-10-11), so there is no image of this
version until then.

On the test server, on f09b60a: go vet ./... ok, go test ./... ok
(23 packages), make e2e ok.

Full detail: CHANGELOG.md (2.3.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:08:37 +03:00
mixandClaude Opus 5.5 eff8010d39 panel-notifications: agreed, taken into work
The owner, 2026-10-12: "Делаем". Roadmap status agreed, 0/10; planned cut
2.3.0. The first step is the mockups, shown to him before any code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:27:20 +03:00
mixandClaude Opus 5.5 972a9f825c release: 2.2.0: a message's page keeps its delivery log
test / test (push) Canceled after 0s
delivery-log-storage (docs/plans/delivery-log-storage.md, 8/8), at the
owner's word (2026-10-12, "Да").

  - The log tailer stores what Postfix writes about queued outbound
    messages; a message's page reads it from the database, so the
    Delivery log box no longer empties when mail.log rotates. The lines
    live as long as the send-log row and go with it.
  - Mail that came in on port 25 is not stored.
  - Migration 0003 (send_log_lines), DDL only.

What an operator should know: the stored lines are 1-2 KB a message
and are in a full backup, which mail.log is not (security.md). A
message accepted before this version shows its lines only while they
are in the current file.

Cut: CHANGELOG [Unreleased] -> [2.2.0]; image pin 2.2.0 in compose,
README, guide; version table, roadmap, plan status. No tag: the owner
tags when he decides to (2026-10-11), so there is no image of this
version until then.

On the test server, on a990e7c: go vet ./... ok, go test ./... ok
(22 packages), make e2e ok.

Full detail: CHANGELOG.md (2.2.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:59:10 +03:00
mixandClaude Opus 5.5 4468aa8dd7 delivery-log-storage: agreed, plan and checklist
The owner agreed the item on 2026-10-12 (option 1 of the roadmap entry: store
the lines). plans/delivery-log-storage.md records what that means in detail:
every mail.log line headed by a queue id goes into send_log_lines with the
line's own time; a line is a row's when the id matches and its time lies
within the row's queue lifetime, which is the bound against Postfix reusing
ids; a line whose id has no send-log row is deleted after an hour, which is
both the retention and the clean-up of inbound and bounce lines; the page
reads the table and falls back to the current file for rows older than it.
Eight checklist steps, cut 2.2.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:05:08 +03:00
mixandClaude Opus 5.5 ae06d8b47e release: 2.1.0: Inbound log and filter lists; a milter security fix
test / test (push) Canceled after 0s
inbound-antispam-panel (docs/plans/inbound-antispam-panel.md, 12/12),
at the owner's word (2026-10-12, "Да").

  - Inbound mail is recorded: accepted mail by a second, read-only
    milter on port 25 with the spam filter's verdict, refused mail from
    the mail log. The filter signs what it judged, and a verdict is
    shown only next to its signature.
  - Inbound > Log, with a sheet over the page that says who decided
    and why; Inbound > Filter lists, allow and deny entries written as
    rspamd maps, each with how often it decided a message.
  - Migration 0002 - the first after the 2.0 baseline.

Security, outside the feature and in every release since 1.9.0: the
level-2 rate limit and an application's client IP list held only for
the first message of a connection. Fixed; e2e sends two MAIL FROM on
one connection.

Upgrading an installation with the antispam sidecar: copy
deploy/antispam/ again (the fragment mounts two new things) and
restart the sidecar. Port 25 now logs as postfix/inbound.

Cut: CHANGELOG [Unreleased] -> [2.1.0]; image pin 2.1.0 in compose,
README, guide; version table, schema-migrations, roadmap, plan status.
No tag: the owner tags when he decides to (2026-10-11), so there is no
image of this version until then.

On the test server, on 1f44e40: go vet ./... ok, go test ./... ok
(22 packages), make e2e ok.

Full detail: CHANGELOG.md (2.1.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:12:16 +03:00
mixandClaude Opus 5.5 d65d632505 release: 2.0.1: DMARC ingest drops and bounds; Full backup from the panel
test / test (push) Canceled after 0s
A patch on 2.0.0, all of it found on the test server after the cut:
  - a message that is not a report this relay keeps is dropped at once
    instead of being retried for days;
  - a report is bounded before it is stored, and reports are kept per
    domain (security review of the ingest path, nothing Critical or High);
  - Full backup from the panel works (it had answered 200 with ten bytes
    since 1.3.0);
  - no backup holds the Postfix queue, and the panel says why.

Cut: CHANGELOG [Unreleased] -> [2.0.1]; image pin 2.0.1 in compose,
README, guide; version table.

On the test server, on dd0fefd: go vet ./... ok, go test ./... ok
(22 packages), make e2e ok.

Full detail: CHANGELOG.md (2.0.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:14:00 +03:00
mixandClaude Opus 5.5 403ce93990 release: 2.0.0: Panel redesign; new URLs; database from zero
test / test (push) Canceled after 0s
The panel redesign (docs/plans/panel-redesign.md, 27/27). A breaking
release: every panel URL changed and the database starts from a single
baseline - a 1.x /data is not upgraded.

Since the last cut also fixed, all found on the test server:
  - the panel can read the mail queue;
  - port 25 answers with DMARC report ingest on, also next to the
    inbound relay;
  - DMARC reports reach the panel (the delivery service never worked
    since 1.7.0);
  - Postfix start-up warnings about the queue directory.

Cut: CHANGELOG [Unreleased] -> [2.0.0]; image pin 2.0.0 in compose,
README, guide; roadmap, plan and version table.

On the test server, on 252e5d1: go vet ./... ok, go test ./... ok
(22 packages), make e2e ok.

Full detail: CHANGELOG.md (2.0.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:32:08 +03:00
mixandClaude Opus 5.5 ed3cb4aaac docs: the panel as it is in 2.0
Stage 3, step 3. Written by Sonnet from the code and the plan, checked
by Opus.

  - guide.md: a section on the panel - menu, paths, the roles global and
    domain with outbound and inbound assigned separately -, Overview and
    Health, Queue and System log, Account and Server > Settings, the
    domain page / Domain settings / application form, Help as a page of
    topics. The DMARC report address is one of three choices set in
    Domain settings. The Spam filter box, the two INBOUND_ANTISPAM_*
    variables the panel reads, and the filter's socket on Health.
    Sessions; backup and export encrypted unless the box is unticked;
    1.x data is not restored into 2.0.
  - architecture.md: Panel HTTP surface rewritten from web.go (routes,
    the /server/ guard, per-object checks, <page>/fragment, one layout,
    typed page data, the kit and its guards, CSP and htmx); Persistence
    gains the database - one baseline, a 1.x file refused, the tables as
    they are.
  - security.md: roles and what each reaches, as built; sessions by user
    id and what ends them; state-changing requests; errors shown to
    users; the 2.0 review; two accepted risks from it - a delegated
    inbound upstream may be any host and port, and Account's profile is
    saved without the password.
  - CHANGELOG: Removed opens with "2.0 starts from an empty data
    directory"; entries that describe the user-default report address
    say they were replaced before release; the filter's socket.
  - roadmap.md: two review follow-up rows struck as retired by the
    redesign (.pair / .split, the two back links); password-reset notes
    that changing the account e-mail must then ask for the password.
    Progress 25/27.
  - development.md: the component kit and its guards; README and
    product.md: wording.

Left as they are
  - The third follow-up row the plan expected to strike - initShowWhen
    is not re-run after an htmx swap - still holds: no polled fragment
    carries such a control today, so nothing breaks, but the redesign did
    not retire it.
  - guide.md embeds no screenshots, so none needed retaking.
  - README and guide still name image 1.9.5 - the version cut.
  - plans/preflight.md and plans/inbound-antispam-panel.md use pre-2.0
    paths; roadmap's template-data-typing is largely done by the
    redesign.

  go test -count=1 ./cmd/panel (env-doc tables): ok

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:01:18 +03:00
mixandClaude Haiku 5.5 2fa0bef6c2 panel-redesign: vendor Bulma 1.0.4 and the Tabler icon subset
Stage 0, step 1. Nothing links the new files yet; the old panel is unchanged.

  - static/bulma.min.css: css/bulma.min.css of npm bulma@1.0.4, byte for
    byte (tarball integrity checked against the registry;
    sha256 67fa26df1ca9e95d8f2adc7c04fa1b15fa3d24257470ebc10cc68b9aab914bee).
  - static/tabler-icons.css + tabler-icons.woff2: @tabler/icons-webfont
    3.49.0 subset with fontTools pyftsubset to the 57 icons the mockups use
    (the plan said 58; the count is 57).
  - NOTICE and development.md § Vendored front-end list both, MIT.
  - Roadmap: panel-redesign is agreed (owner, 2026-10-09), 6/27; the entry no
    longer says 29 screens or that Health is undecided.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-09 17:19:23 +03:00
mixandClaude Opus 5.5 57c6fddfc3 release: 1.9.5: GHCR release fix: keep per-arch images, sweep orphaned versions (CI only)
test / test (push) Canceled after 0s
CI-only cut. The release workflow deleted the X.Y.Z-amd64 / X.Y.Z-arm64
package versions after the manifest merge; those versions are the images the
X.Y.Z manifest points at, so pulling 1.3.0 failed with a missing manifest.
No product code changes.

Fixed:
  - release.yml: the per-arch tags stay in GHCR. GHCR cannot drop a tag
    without deleting the image behind it.
  - release.yml: the merge job now removes only untagged package versions
    that no tagged manifest references (leftovers of publishing a version
    again); any failed lookup aborts the step before anything is deleted.
  - development.md release description and version table, image pin 1.9.5
    in compose, README, guide.

Full detail: CHANGELOG.md (1.9.5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:25:34 +03:00
mixandClaude Fable 5.1 1e92c5b2a2 release: 1.9.4: Panel-redesign plan: component kit, stages, reorder (docs only)
Plan-only cut, owner-requested. The redesign goes first as 2.0.0 and
inbound-antispam-panel follows as 2.1.0 on the 2.0 baseline (migration
0002_inbound_spam_log.sql), built from the redesign's component kit so its
two screens are styled once. No product code changes.

Changed:
  - panel-redesign plan: Component kit section and stage 0 (partials with
    typed inputs in components.go, one panel.css, /server/components kit
    page, acceptance before any page is restyled); staged checklist (5/27);
    Health decided inside 2.0, queue pages left as old content in the new
    shell.
  - panel-redesign plan: Against resurrected pages - shrink-only
    legacyPages ratchet in the guard tests enforced by CI, the outline
    golden, the old-path test and a history-blob check, so an old template
    from git history cannot pass as a redesigned page.
  - roadmap: new candidate delivery-log-storage - the Delivery log box on a
    message page is empty for anything older than the last daily rotation
    because the lines are grepped from the current mail.log on each view
    and never stored.
  - development.md version table, image pin 1.9.4 in compose, README, guide.

Full detail: CHANGELOG.md (1.9.4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 00:15:48 +03:00
mix 6bfaa518d7 release: 1.9.3: Panel-redesign prep: mockups, routes, design contract (docs only)
test / test (push) Canceled after 0s
Panel-redesign prep, docs only: stage 1-2 mockups on all 33 screens, the accepted header/type-scale/contrast corrections, the account e-mail and inbound-delegation decisions, and the routes/quarantine/preflight roadmap screens. Nothing in `internal/web` changes; the design stays `candidate` (3/21) until the redesign itself ships as `2.0.0`.
Added:
  - Panel redesign, account e-mail and notifications: a user's e-mail is its own
  - Panel redesign, contrast: a sweep of every text node against its real ground
  - Panel redesign, header (owner's choice: variant A, with the perforation on a
  - Panel redesign, type scale: about 40 % of the panel's text measured under
  - Panel redesign, header comparison sheet
  … (8 more in CHANGELOG)

Full detail: CHANGELOG.md (1.9.3).
2026-09-22 15:24:53 +03:00
mix 7ef7f75246 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).
2026-09-22 15:24:53 +03:00
mix 967d3d95de release: 1.9.1: Port-25 Postfix startup with inbound/DMARC/antispam flags
Port-25 Postfix startup when inbound relay, DMARC ingest, or the inbound antispam milter are enabled. Schema-migration reference and roadmap plans for inbound antispam and quarantine. Upgrading from 1.9.0 is a tag bump; no migration.
Added:
  - [docs/plans/inbound-antispam-panel.md](docs/plans/inbound-antispam-panel.md) —
  - [docs/plans/inbound-quarantine.md](docs/plans/inbound-quarantine.md) — candidate
  - [docs/schema-migrations.md](docs/schema-migrations.md) — living reference for the
Changed:
  - [docs/roadmap.md](docs/roadmap.md) — `inbound-antispam-panel` (agreed, 1/12)
  - [docs/development.md](docs/development.md) — plan checklists may override
  - [docs/development.md](docs/development.md), [docs/roadmap.md](docs/roadmap.md),
Fixed:
  - `build/postfix-config.sh`: enabling inbound relay, DMARC ingest, and/or the

Full detail: CHANGELOG.md (1.9.1).
2026-09-22 15:24:53 +03:00
mix 20d88e1d9c release: 1.7.0: DMARC aggregate report ingest and panel summaries
Optional DMARC aggregate report ingest: accept `rua=` mail on port 25, parse gzip/XML into SQLite, and show summaries in the panel.
Added:
  - Optional DMARC aggregate report ingest (`DMARC_REPORTS_ENABLE`): port 25
  - Per-domain **SelfPost hosted** `rua=` mode when ingest is enabled

Full detail: CHANGELOG.md (1.7.0).
2026-09-22 15:24:52 +03:00
mix 60b743e6c2 release: 1.6.0: 30-day send stats and optional auto level-2 rate limits
30-day sending statistics per domain and application, plus optional auto level-2 rate limits derived from average send rate.
Added:
  - Domain page **Sending statistics** card: total messages, peak and average
  - Level-2 rate limit **Manual** / **Auto** mode on domain and application
  - Domain export JSON now includes rate limits (`mode`, `auto_multiplier`,
Changed:
  - Auto mode uses the level-1 window (`RATE_LIMIT_WINDOW_SECONDS`); manual

Full detail: CHANGELOG.md (1.6.0).
2026-09-22 15:24:52 +03:00
mix dca7d99c1b release: 1.5.0: Send-log retention configurable in panel Settings
Send-log retention is configurable from panel **Settings** (global administrator); `SEND_LOG_RETENTION_DAYS` seeds the initial default only.
Added:
  - Panel **Settings** (global administrator): **Send log retention (days)** —
Changed:
  - Deliveries list and delivery detail pages show the configured retention

Full detail: CHANGELOG.md (1.5.0).
2026-09-22 15:24:52 +03:00