Commit Graph
63 Commits
Author SHA1 Message Date
mixandClaude Opus 5.5 2dded921f5 release: 2.13.0: local-time - the panel in the container's time zone
test / test (push) Waiting to run
No migration; schema 12. The pins (README, compose, guide), the guide's
backup example, the planned cuts and the roadmap row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 01:35:42 +03:00
mixandClaude Opus 5.5 bdeb01681d local-time: the panel in the container's zone, named on Settings
Agreed by the owner on 2026-10-13 (the local day, no "UTC" beside a
time); the zone is the container's TZ in .env and Settings only shows it,
his "Да" of 2026-10-17. Mockups c205106.

Every time the panel shows is formatted in time.Local and says no zone:
the logs' Time and the DMARC Received columns, a message's timeline,
Overview's "Updated", Health's certificate and renewal, the filter lists,
DMARC reports and windows, the auto limit's last recalculation. Stored
instants stay UTC. The days counted are the zone's: delivery_log_days and
sender_check_days key their rows by the local date, the sender check cuts
downtime at local midnights (byDay), the log filters' days run from local
midnight to local midnight. Notification mail and the test message write
the zone's abbreviation after a time - they are read away from the panel.
DMARC's one-day window label stays the reporter's (UTC) day.

Settings box 05, Time zone, read-only: the name (UTC without TZ), its
offset, and a TZ the image cannot load said to leave the times in UTC
(time/zoneinfo_unix.go, initLocal) rather than named. TZ is passed by the
compose file ("${TZ:-}"), in .env.example and the guide's table; tzdata is
named in the image (supervisor's python3.13 brings it today). Postfix
takes TZ through import_environment, so mail.log's stamps are the same
zone, which closes the trap the roadmap wrote down: the tailer has always
read them in time.Local.

Tests: a TestMain runs view, handlers, store and cmd/panel in UTC whatever
the machine's zone (as logtail's); TestSettingsShowsTheTimeZone,
TestTimesAreTheContainersZone, TestByDayInTheContainersZone, a Moscow day
in TestQueryInboundSpamFilters; envdoc's pattern takes two-letter names.
e2e time_zone_is_the_containers recreates the stand with
TZ=Europe/Moscow: date says MSK, Settings names the zone and UTC+03:00, a
test message's page gives its time and its mail.log lines in Moscow's.
go vet + go test (26 packages) and make e2e on the test server for this
tree.

Docs: guide (§ Settings, the TZ row, the days of Health and the logs),
architecture, schema-migrations, help (word for word with the mockup),
CHANGELOG [Unreleased] § Added, roadmap row done with the decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 01:34:28 +03:00
mixandClaude Opus 5.5 4ff032a463 release: 2.12.0: test-email - a test message from Server › Health
test / test (push) Waiting to run
Cut without asking, as the owner said on 2026-10-15. A global user sends
one message from postmaster@ the hostname to an address typed; it opens in
the Outbound log, which follows it (the commit before this; mockups
e92769f, e3b2501, 8f0ccf9). Reviewed for security, its low findings fixed.
go vet, go test (26 packages) and make e2e passed on the test server for
that tree. No migration, schema 12.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:42:27 +03:00
mixandClaude Opus 5.5 546a1aa461 release: 2.11.0: health-setup-checks - name, greeting, proxy on Health
test / test (push) Waiting to run
Cut without asking, as the owner said on 2026-10-15. Server › Health checks
the certificate's name against the hostname, shows Postfix's greeting and
where the panel sees the viewer from (the commit before this; mockups
b2dcefc, cc33890, 0feaa55). go vet, go test (26 packages) and make e2e
passed on the test server for that tree. No migration, schema 12.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:51:43 +03:00
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 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 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 b09e525521 deploy: extract-cert.sh is executable
The guide runs it as ./traefik/extract-cert.sh, and git held it 100644 from
its first commit (review of audit stage 5, L3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:53:38 +03:00
mixandClaude Opus 5.5 a39eda5a0b audit 5: findings of the two reviews
- The e2e's numeric-settings test ran a container that passed every gate
  and never ended: the good values are checked beside a bad one after
  them; each run is named, bounded to a minute and removed.
- The gates refuse a value under the parameter's minimum (a zero window
  kills anvil, a zero milter timeout smtpd) and one past an int, in
  seconds for a time value (src/global/conv_time.c, src/anvil/anvil.c).
- OpenDKIM waits, five seconds at most, for the relay's socket.
- The relay marks its lines `syslog: `, writes control characters out,
  reads into a queue at once and drops past 200 lines a second, saying
  how many (security F1, F2; a stuck relay would hold OpenDKIM in
  syslog(3)).
- smtp_tls_mandatory_protocols >=TLSv1.2 too.
- extract-cert.sh keeps the old files' group and sets its trap before
  mktemp.
- crashexit spares cert-reload; architecture.md "four times running" and
  what else exits the container; security.md the restart after five panel
  exits and the /dev/log socket.
- Measured against the technical review: smtpd has the selfpost group
  (Groups 104 998), so it did reach the supervisor socket before this
  stage.
Accepted, reasons in the plan: the logrotate state file in the postfix
user's directory, envdoc_test's reading of `${VAR:-}` only, crashexit's
window pruning untested.

Tested on the test server: go vet + go test of 27 packages, make e2e (all
pass, 592 s) on this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 13:53:14 +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 cad1182150 audit 2c: what only the stand could answer, measured and written down
test / test (push) Canceled after 0s
All 2026-10-15; each written beside the code or text it settles.

- A-43: /usr/sbin/postlog is -rwxr-xr-x root:root in the image (postfix
  3.7.11-0+deb12u1; dpkg-statoverride lists postdrop and postqueue only):
  Debian does not install it set-gid, so it is no use to dmarc-ingest
  (cmd/panel/dmarc_ingest.go).
- C-X1: OpenSSL 3.0.22 in the image prints the same public key from
  `x509 -pubkey` and `pkey -pubout` for RSA-2048, P-256, P-384 and a SEC1
  P-256 key - cert-reload's same_pair holds for certbot's ECDSA keys.
- C-K5, D-guide:933/:233: on the live server (Docker 29.6.1, userland
  proxy on 0.0.0.0 and ::, no IPv6 on the network) an outside IPv4 client
  and the host's own public IPv4 address are logged as themselves; the
  host's IPv6 address and 127.0.0.1 arrive as 172.18.0.1. Over IPv6 every
  client is the gateway: one level-1 budget, an application IP list cannot
  name them, rspamd sees the gateway. Guide paragraph under level 1, notes
  at the milter and allow-list, comments at the anvil setup and authip.go,
  and a Security entry in the CHANGELOG (an allow-list naming the gateway
  would admit every IPv6 client).
- D-guide:61: a published port nothing listens on (a throwaway container,
  -p 20999:25) is refused from outside over IPv4 and from the host's public
  IPv4, accepted and closed through 127.0.0.1 - Docker's proxy, which IPv6
  goes through too (not probed over IPv6 for this).
- C-M8: the live installation has received one DMARC report, of one
  record: nothing to set maxReportRecords by yet - said at the constant.
- D-S7, D-arch:516, D-arch:532: a new database filled through the store to
  120 days at 1 900 messages a day (228 000 messages, 1.37 million lines,
  464 MB) on the test server (2 cores): the longest write is the line prune
  after a 30-day gap, 1.9 s for 342 000 lines (0.4 s with nothing to
  delete); the send-log delete 0.6 s; 2 000 lines of a tailer pass 18 ms;
  VACUUM INTO 1.7 s; a second connection writing every 50 ms waited 1.8 s
  at most, no SQLITE_BUSY - inside busy_timeout's 5 s. architecture.md's
  "about a second per million lines" was five times too fast; its "six to
  ten lines of 200 bytes" is 6.0 lines of 146 bytes on the live data.
- D-B16: which favicon is fetched, by the probe server's log: Edge 155
  the SVG, the desktop app's embedded Chromium 152 the PNG, with or
  without sizes="any" - layout.html's "vector wins where supported, raster
  catches the rest" holds; Firefox and Safari not measured.
- D-arch:121: the allow/deny trial again, in rspamd/rspamd:3.11.1-3 with
  today's multimap.conf: every mix of allow and deny entries matching one
  message ended reject; a changed map was used about 10 s later.
- D-sec:424: rspamd runs as _rspamd, uid 11333; the panel is uid 999.

Stand: go vet and go test ./... pass on the server (24 packages); make
e2e exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:14:58 +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 2e1544dac5 certbot: certs/ links into /etc/letsencrypt, mounted at the same path; the backup follows them
test / test (push) Canceled after 0s
C-A1, C-Q4, D-guide:258, :291, :722. certbot's live/<host>/*.pem are
relative links into ../../archive/<host>/ (eff-certbot docs, "Where are
my certificates?": point a server at them "or create symlinks"). The
guide had `ln -s /etc/letsencrypt/live/<host> certs` and the nginx
fragment mounted live/<host> alone: inside the container the links led
into a directory it did not have - postfix-config.sh's [ -f ] false, no
copy, Postfix without a certificate - and the panel's backup, reading
certs/ through /selfpost-deploy, skipped links silently.

Now certs/fullchain.pem and certs/privkey.pem are links to
/etc/letsencrypt/live/<host>/, and /etc/letsencrypt is mounted read-only
at the same path: a commented line in deploy/docker-compose.yml for the
Apache scenario, mounted in the nginx fragment (./nginx/certbot-etc). The
guide's steps add certbot's own commands for a server that is not root -
chmod 0755 live archive, chgrp 998 + chmod 0640 the key, which certbot
keeps on renewal - so the panel reads them (Health, backup). Postfix's
side follows links already ([ -f ], cp, cmp, openssl). The vhost comment
says the same. The backup table: links are followed by the panel's
backup; a stopped tar stores them as links unless -h (GNU tar).

internal/backup: addTree follows, for certs/ only, a link to a regular
file and archives that file under the link's name; a link to nothing
reachable is an error naming it and the mount.

Tests: TestCertsLinksAreFollowed (certs/ -> live/ -> archive/, the
archive holds the files; the tree gone, Check names the dangling link;
Linux). Stand (2026-10-15, make e2e, exit 0): the stand mounts
./letsencrypt at /etc/letsencrypt; new certbot_layout_followed puts a new
pair into letsencrypt/archive/, relative links in live/, absolute links
in certs/, recreates the container: 465 serves the new serial, Health
names no renewal waiting, and the full backup holds certs/*.pem as
regular files with the archive's bytes. go vet and go test ./... pass on
the server (24 packages); the Linux-only tests of this session run there
and pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:34:19 +03:00
mixandClaude Opus 5.5 31c2b50457 antispam: rspamd strips a sender's own verdict for every client
test / test (push) Canceled after 0s
rspamd 3.11.1's milter_headers skips its verdict routines - removes none
of the sender's X-Spamd-Result / X-Rspamd-Action, adds none - for a client
it counts as local (loopback, a unix socket, local_addrs: the private
ranges) or one that authenticated (skip_local, skip_authenticated, both on
by default; milter_headers.lua skip_wanted(), lua_ip.c lua_ip_is_local()).
The custom routine that signs X-SelfPost-Filter is never put through
skip_wanted(), so it signed the sender's own verdict, and Inbound > Log
showed it as rspamd's. Every sender is private when Docker's proxy relays
port 25 from the bridge gateway. Both options set to false.

Tried 2026-10-14 in a throwaway rspamd/rspamd:3.11.1-3 on the test server
(rspamc --ucl, a message carrying its own verdict headers): old config,
from 10.0.0.5 and as an authenticated user, the milter reply removed and
added neither and added the signature; new config, both removed and
rspamd's own added, as from 203.0.113.9.

Also: the sender rules take the envelope only (extract_from = "smtp"; by
default a bounce's From: header is used - tried: a bounce whose From:
domain is denied was refused before, is not now; a denied sender is
refused either way); the image pinned to 3.11.1-3, the build read (3.11 and
3.11.1 move with each rebuild); the prefilter lines no longer read go.
architecture.md and security.md say so. Assumptions C-R7, B-R1, C-R4,
C-R12, C-R5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:48:07 +03:00
mixandClaude Opus 5.5 599149bf83 build, deploy, inbound, dmarc, backup, e2e: cite the source of what they rely on (assumptions part C)
Comments only, no behaviour changed. Each main.cf/master.cf parameter the
build sets, the wrapper's and the reload hook's reliance on Postfix 3.7.11
(postfix-script, master.5, postconf.5, cleanup_init.c, smtpd_peer.c,
README_FILES), supervisor 4.2.5 (process.py, options.py, events.rst),
logrotate 3.21.0, OpenDKIM's config, Cyrus SASL's sasldb, rspamd 3.11.1
(milter_headers.lua, multimap, the map watcher), Docker Compose, CoreDNS
and certbot now say where that is written. Measurements already there stay
measurements ("date not recorded" where none was); hand-composed fixtures
say so.

Contradictions left for their own commits: C-P31 cleanup's size limit,
C-P42 master outside supervisor's signal, C-V5/V6 startsecs, C-V10,
C-S3/Q3 sasldb written in place, C-Q4/A1 certbot's symlinks, C-R7 rspamd
skips local clients but the signature routine does not, C-P8, C-R4, C-R12,
C-M7, C-E2, C-K8/K9, C-D1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:39:21 +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 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 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 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 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 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 1f44e40382 inbound-antispam-panel: the filter reads its signing key for every message
test / test (push) Canceled after 0s
The routine kept the key after its first read. SelfPost makes a new key
when the file is lost or damaged, or a backup brings back another, and
rspamd would then have gone on signing with the old one - every verdict
unrecorded - until someone restarted the sidecar. The file is one short
line; reading it per message costs nothing worth keeping a copy for.

The guide says what never needs doing: nothing of rspamd's is cleared
after a change of the lists or the key; a restart is for a change of
its own configuration only.

Prompted by the owner's question whether rspamd's cache needs clearing
(2026-10-12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:42:16 +03:00
mixandClaude Opus 5.5 d398f488e2 inbound-antispam-panel: the filter signs what it judged
test / test (push) Canceled after 0s
Closes what the security review left as an accepted risk: with the
spam filter down - it is attached fail-open - nothing removes verdict
headers a sender wrote himself, and the inbound journal recorded his
verdict as the filter's. The owner asked whether rspamd could add a
header that proves the others are ours (2026-10-12) and agreed to a
signature: a fixed mark could be written by a sender as easily as the
verdict.

  - inbound.FilterKey: a key of 32 random bytes, 64 hex characters, in
    /data/antispam/filter.key, made on the first start with the relay
    on. 0644 like the list files beside it: the filter is another user
    in another container.
  - deploy/antispam/rspamd/milter_headers.conf: a custom routine reads
    the key through the directory the sidecar already mounts and adds
    X-SelfPost-Filter, the HMAC-SHA256 of the message's queue id, hex;
    a header of that name from the sender is removed.
  - internal/milter/inbound.go: the verdict headers are kept as they
    are read, and at end-of-message - when the queue id is known - they
    count only if the signature fits (FilterSignature, hmac.Equal). No
    key, no queue id, no signature or a wrong one: the row is that of
    mail nobody judged (accept, selfpost, no detail).

The signature goes on to the upstream with the mail and is of no use
there: it fits one queue id.

Tried in a throwaway rspamd 3.11 container on the test server before
the Go side was written: the routine loads, signs
(2aa1ca6d... for key 0123...cdef and queue id 4A1B2C3D5E, the value
python's hmac gives and TestFilterSignatureIsWhatTheFilterWrites
pins), removes a sender's header of the same name, and adds nothing
without a queue id.

Tests: signed, unsigned, made-up, another message's, another key's,
the sender's followed by the real one, the next message on the
connection, no queue id; the key made once, read after, replaced when
damaged, left alone by the lists. e2e: filter.key is there, 644, 65
bytes.

docs/security.md: the accepted risk is replaced by a narrower one (the
key is readable on the host); Mail path describes the signature.

Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:34:29 +03:00
mixandClaude Opus 5.5 7192c3861e inbound-antispam-panel: Filter lists, kind column, matches, note on a sheet
Follows the design commit before it (owner's words quoted there).

The lists' tables are Value, Kind, Matched and Remove. The kind tag
opens the entry's sheet: note, day added, how often it decided a
message in the days the log keeps, and the last match.

Matches are read off the journal on each view, nothing is stored
(inbound.Lists.Usage, store.ListSpamListHits). A row counts for an
entry when the list decided the message - a rejected row whose reply is
the deny rule's message, an accepted row whose verdict carries the
allow rule's symbol for that kind - and the entry fits the row's sender
or client address the way rspamd matches it: an address whole, a domain
as the sender's domain, a network containing the client (an
IPv4-mapped address as IPv4). A failure to count shows the lists with
"never" and logs why.

For that the journal milter keeps the lists' own symbols ahead of its
six-symbol cap: rspamd writes symbols heaviest first, so an allow
entry's -100 came last and was cut behind any six others.

The plan gains "Filter list usage": what counting from the journal
implies, and - the owner asked for it in writing - the conditions under
which a counter kept with each entry becomes worth a migration (the
journal cut by its row cap and not by age; a short retention; counts
that must outlive the journal; a slow page). multimap.conf says which
of its texts SelfPost reads back.

Tests: Usage over every kind on both lists and the rows that are
nobody's; the milter's symbol order; the store query; the page and its
sheets; a handler test from journal rows to the numbers on the page.

Seen: the fixture page with a sheet open, headless Edge at 1360 px -
the same as the accepted drawing.

Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok.

Not seen in a real run: a count above zero. It needs an entry added in
the panel, which I cannot sign in to, and then mail that the list
decides; in particular the deny rule's message arriving in mail.log as
the reply of a milter-reject line is what the trial with rspamc showed,
not yet what Postfix logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:08:26 +03:00
mixandClaude Opus 5.5 eea40bcd49 inbound-antispam-panel: filter lists, written as rspamd maps
Step 5/12 of docs/plans/inbound-antispam-panel.md.

Store: AddSpamListEntry, ListSpamListEntries, DeleteSpamListEntry.
internal/inbound/lists.go validates and normalises an entry - sender
address, sender domain, client address or network (canonical text, an
IPv4-mapped address as IPv4, /0 refused) - and writes the lists as six
map files under /data/antispam (ANTISPAM_DIR), each atomically, 0644 in
a 0755 directory: the filter is another user in another container. All
six are written on every change and on every start with the relay on,
empty ones included. An entry whose maps could not be written is taken
back out of the table. The writer refuses a stored value with
whitespace, a newline or # in it.

The plan's allow.map / deny.map became one file per list and entry
type: to rspamd an IP map and a sender map are different map types.

deploy/antispam/rspamd/multimap.conf holds the rules; the fragment
mounts it and ./data/antispam into the sidecar. Deny is a prefilter
reject, allow a symbol weighing -100. Tried in a throwaway rspamd 3.11
container on the test server:
  - with allow as a prefilter accept, an address on both lists was
    accepted and an IP on both was refused - the rule that ran first
    decided; with the weight, deny won in all five mixed cases;
  - IPv4, IPv6, address (any case), domain (not its subdomains) match;
  - a rewritten file is reread in about ten seconds, no restart;
  - files that appear after rspamd started are loaded the same way.

No page edits the lists yet (step 6).

Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok - with the relay on
/data/antispam is 755 and holds the six maps, 644, empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:07:10 +03:00
mixandClaude Opus 5.5 27fd355e53 inbound-antispam-panel: refused inbound mail from mail.log; retention
Step 4/12 of docs/plans/inbound-antispam-panel.md.

The log tailer writes an inbound_spam_log row for mail port 25 refused
(internal/logtail/inbound.go): a "reject:" line of the port-25 smtpd is
Postfix's own policy; a milter-reject / milter-hold / milter-discard
line of cleanup is the filter's verdict. Only a recipient at an inbound
domain is a row, so "Relay access denied" for a stranger's domain is
not. The row is dated by its log line; the reply text is cut to 255
bytes of printable ASCII; a reply starting with 4 is a tempfail.

To tell port 25 from 465 / 587 the listener now logs as
postfix/inbound (syslog_name). cleanup logs under one name for every
listener, so its verdict counts only for a queue id the port-25 smtpd
was seen to open (4096 remembered). Lines are matched from the head,
after timestamp and host: their tail is the sender's own text.

Retention: PruneInboundSpam every six hours - older than the
inbound_spam_log_retention_days setting (seeded from
INBOUND_SPAM_LOG_RETENTION_DAYS, 90, range 7-365), then down to 10 000
rows. The setting has no field in the panel yet.

Known limits, in architecture.md: a log line names one recipient, so a
message refused at end-of-message is one row; a line re-read after an
unclean stop would be a second row.

Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok - the unknown recipient at
the inbound domain refused on port 25 has its row (reject by postfix),
the stranger's domain has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:42:25 +03:00
mixandClaude Opus 5.5 28ec784ae2 inbound-antispam-panel: inbound journal milter on port 25
Step 3/12 of docs/plans/inbound-antispam-panel.md.

The panel serves a second milter while the inbound relay is on
(internal/milter/inbound.go, socket INBOUND_JOURNAL_MILTER_SOCKET,
default /run/selfpost/inbound-journal.sock). build/postfix-config.sh
puts it last on port 25, after the optional antispam milter, with
default_action=accept; postfix-wrapper.sh waits for its socket. At
end-of-message it writes one inbound_spam_log row per recipient at an
inbound domain - other port-25 recipients (DMARC report addresses) get
none - and always accepts. With a filter attached it reads the verdict
from X-Rspamd-Action and X-Spamd-Result (action, score, up to six
weighted symbols, only what the patterns recognise, 255 bytes); without
one those headers are the sender's and are not read. The plan named
X-Rspamd-Score, which rspamd does not have.

The antispam compose fragment mounts rspamd/milter_headers.conf into
the sidecar so that it adds the two headers (checked in rspamd 3.11:
both routines exist and remove a sender's own headers of those names).

Store: InsertInboundSpam, IsInboundDomain.

Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok - the relayed message of
inbound_relay_delivers_and_refuses has its journal row (accept by
selfpost) and the DMARC report sent through the same listener has none.

Not yet seen in a real run: a verdict written by rspamd reaching this
milter. The e2e stand has no filter, and the test server's only inbound
domain has no upstream, so it accepts no mail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:18:14 +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 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