test / test (push) Waiting to run
Owner, 2026-10-18: "Надо почистить документы от устаревшего и сделанного." - Deleted, shipped: plans/delivery-log-storage, from-header-binding, inbound-antispam-panel, inbound-upstream-result, panel-dkim-signing, panel-notifications, and the own-mta-builds analysis (its three rows shipped as 2.6.1, 2.7.0, 2.8.0). What outlived them moved first: when a filter-list counter is worth a migration, and the notifications' "when" table, into architecture.md; the sender check's own-process trigger into security.md. Code comments that cited them point at architecture.md or security.md; CHANGELOG keeps their names, unlinked. - Kept, marked closed: audit-2026-10-12, assumptions-2026-10-14 and security-audit-2026-10-17 - the code cites their finding ids in 50-84 files. Their file:line refs are of the tree each audited. - roadmap.md: open work only - stage H of the security audit (the owner's), go-milter-upstream (the owner posts), and queue-table, which the redesign left undecided and only its own shipped section still named; Dropped and Ideas as they were. 2.x versioning instead of 1.x. - development.md: the 47-row planned-version-cuts table (history, in the CHANGELOG) gone; a stage's last step now removes its roadmap item and plan. Docs map lists the closed audits. - panel-redesign.md: the done checklist out; Room for roadmap items says which drawn screens are unbuilt and why. - Stale facts fixed: schema-migrations.md and architecture.md at schema 14 (0013's users columns and user_links, 0014's secure TLS mode); security.md's accepted risk "profile saved without the password" - the e-mail asks for it since 2.14.0. Tests: go build, go vet; go test of cmd and the touched internal packages pass except logtail's three Windows-bound rotation tests (rename of an open file; CI is Linux). Only comments changed in Go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
9.7 KiB
Markdown
181 lines
9.7 KiB
Markdown
# Roadmap: open work
|
||
|
||
**Status:** a working tracker of extensions to the product boundary
|
||
([product.md](product.md),
|
||
[.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)), each taken
|
||
up only after explicit agreement. Items marked `candidate` need an OK before
|
||
any code is written; detailed design, when an item needs it, lives in
|
||
[plans/](plans/). A shipped item leaves this file: what it did is in
|
||
[CHANGELOG.md](../CHANGELOG.md), its section and plan stay in git history.
|
||
|
||
**Reading this from outside the project:** nothing here is a commitment or a
|
||
release promise. There are no dates, the order is a recommendation rather than
|
||
a schedule, and an item can be dropped or reshaped once its plan is written.
|
||
What the project *will not* do is a separate question, answered in
|
||
[product.md](product.md) — an item's absence from this file does not mean it is
|
||
planned but unlisted.
|
||
|
||
**Versioning:** SemVer in the 2.x line — MINOR for a feature or a migration,
|
||
PATCH for a fix. A major only for an explicit break; `2.0.0` was one
|
||
([schema-migrations.md](schema-migrations.md) § 2.0 starts from an empty data
|
||
directory).
|
||
|
||
**Process:** [development.md](development.md).
|
||
|
||
---
|
||
|
||
## Index
|
||
|
||
| ID | Topic | Status | Progress | Plan |
|
||
|---|---|---|---|---|
|
||
| security-audit-2026-10-17 | Stage H of the 2026-10-17 security audit: the test server's host (S1, S2, S5) and its DNS (S3) | stages 0-6 shipped `2.14.4`, the product side of S3 `2.15.0`; stage H the owner's | 27/29 | [security-audit-2026-10-17.md](security-audit-2026-10-17.md) § Stage H |
|
||
| go-milter-upstream | An issue and a PR for go-milter: its header map outlives a message and is unbounded | prepared 2026-10-16, the owner posts | — | [third_party/go-milter/PATCHES.md](../third_party/go-milter/PATCHES.md) |
|
||
| queue-table | Outbound › Queue as the table the mockup draws, from `postqueue -j`; an Inbound › Queue (`in-queue`) | candidate, undecided since 2026-10-09 | — | — |
|
||
|
||
After a context reset, pick an item marked `agreed` or `in progress`, then work
|
||
the **Implementation checklist** in its linked plan. The `Progress` column above
|
||
is `done/total` checklist steps in that plan ([development.md](development.md)
|
||
§ Plan checklists).
|
||
|
||
---
|
||
|
||
## security-audit-2026-10-17
|
||
|
||
The audit's stages 0-6 shipped together as `2.14.4`, the server's DMARC hint
|
||
(the product side of S3) and Verified TLS to the upstream (M7) as `2.15.0`.
|
||
What is left is stage H, the owner's host and DNS, about an hour of his time
|
||
and no product code: S1, S2 and S5, whose commands are in
|
||
`dev/security-audit-2026-10-17-host.md` (not published), and S3's records -
|
||
`_dmarc.selfpost.mixfed.ru TXT "v=DMARC1; p=reject; rua=mailto:<an address
|
||
you read>"` and `mixfed.ru CAA 0 issue "letsencrypt.org"`. The report's
|
||
§ Stage H has the "done when" of each.
|
||
|
||
---
|
||
|
||
## go-milter-upstream
|
||
|
||
**Problem (owner, 2026-10-16):** the panel builds go-milter v0.4.1 patched
|
||
(`third_party/go-milter`, its `PATCHES.md`): upstream adds every header of
|
||
a session to a map it hands to `Headers` and empties only on ABORT. Two
|
||
things follow, for every user of the library: the map of a second message
|
||
on one connection still holds the first message's headers (SelfPost met it
|
||
as audit 2026-10-12, L9, and keeps its own headers since), and the map is
|
||
bounded by no limit of its own - a message of nothing but headers, up to
|
||
the MTA's message size limit, is held whole in each milter. Upstream had no
|
||
issue about it on 2026-10-16; its last commit was 2026-04-04.
|
||
|
||
**Goal:** an issue and a PR, prepared here and posted by the owner from his
|
||
GitHub account (nothing is sent from this side):
|
||
|
||
- the issue: both effects, each with a reproduction through the library's
|
||
own client (two messages on one connection; the map's size at EOH);
|
||
- the PR, small and compatible: the map emptied when a message starts
|
||
(`CodeMail`), as it is on ABORT - this fixes the first effect and bounds
|
||
the second to one message;
|
||
- in the issue, as a proposal: an option of `Server` not to keep the map,
|
||
for a milter that reads headers through `Header` alone - what SelfPost's
|
||
patch does unconditionally, which upstream cannot take as it is (it would
|
||
break backends that read the map).
|
||
|
||
**Done when:** the texts and the branch are ready for the owner to post.
|
||
|
||
*Prepared (2026-10-16):* in `dev/upstream-pr` (not in git): `ISSUE.md`,
|
||
`PR.md`, a README with the steps, and a clone of go-milter on branch
|
||
`reset-headers-per-message` - one commit over `master` (0277691) that
|
||
empties the map on SMFIC_MAIL, with `TestMilterClient_HeadersPerMessage`,
|
||
which fails on `master` (the second message sees both `From` values)
|
||
and passes with it. Read for the issue: Postfix carries every message of
|
||
one SMTP session over the one milter connection smtpd opened (Postfix
|
||
3.10.13, src/milter/milter8.c:2723, LOCAL_SEND_FD of the milter socket to
|
||
cleanup; src/smtpd/smtpd.c:3475, milter_send()).
|
||
When upstream releases a version that does not keep the map (or offers the
|
||
option), `third_party/go-milter` goes and `go.mod`'s `replace` with it.
|
||
|
||
---
|
||
|
||
## queue-table
|
||
|
||
**Problem:** the 2.0 mockups draw Outbound › Queue as a table and an
|
||
Inbound › Queue (`in-queue`); the redesign kept both out as decisions of
|
||
their own (owner, 2026-10-09). Outbound › Queue shows `postqueue -p`'s own
|
||
listing in a log pane, and the mockup's `in-queue` is drawn but not built
|
||
(`deferredScreens`, `outlineExempt` in the guard tests).
|
||
|
||
**Goal:** the outbound queue as a table from `postqueue -j` - a row per
|
||
message with its sender, recipients, age and last reason - and, if the owner
|
||
wants it, the same for mail port 25 holds for an upstream.
|
||
|
||
**Recommendation:** the outbound table first, read-only - about 4 h with its
|
||
tests, since the notifier already reads `postqueue -j` for each message's
|
||
arrival time; the inbound queue and any action on a row (retry, delete) only
|
||
if the owner asks for them, each drawn mockup-first.
|
||
|
||
---
|
||
|
||
## Dropped, with the condition for return
|
||
|
||
Items the owner took off the roadmap. Each names what would bring it back;
|
||
without that it is not offered again.
|
||
|
||
- **structured-logging** (dropped 2026-10-13) - move the panel from
|
||
`log.Printf` to `log/slog` with levels and fields, one logger passed through
|
||
constructors. Dropped because the panel's log has one reader, the owner, by
|
||
eye in `docker logs`; the change is wide (some 225 call sites), changes no
|
||
behaviour, and gives a new format to everything that reads the output.
|
||
**Returns if:** the log goes to an outside collector, or it has to be
|
||
filtered by level.
|
||
- **Certificate warnings measured by the certificate's lifetime** (left as
|
||
it is, owner, 2026-10-14). The panel warns 14 and 3 days before a
|
||
certificate expires, whatever its lifetime
|
||
([notify.go](../internal/notify/notify.go), `certFirstDays`,
|
||
`certLastDays`). A certificate issued for less than 14 days is inside the
|
||
first warning from its first hour, and each renewal of it is a new
|
||
certificate and a new message. Not reachable with the 90-day certificate
|
||
the instance has. **Returns if:** the instance takes a certificate that
|
||
lives less than 30 days. The rule then offered: the first warning at a
|
||
quarter of the lifetime left, the last at a tenth, and never earlier than
|
||
the 14 and 3 days of today - so a longer certificate, a yearly one
|
||
included, is warned about exactly as now.
|
||
- **contributing** (dropped 2026-10-13) - a `CONTRIBUTING.md` in the root:
|
||
the dev loop, the checks before a PR, the commit protocol. Dropped because
|
||
there is one developer and no PRs, and everything the file would say is in
|
||
[development.md](development.md); a second file is a second copy to keep
|
||
in step. **Returns if:** the first outside PR arrives, or the owner decides
|
||
to invite contributors.
|
||
- **A check that ports 25, 465 and 587 are reachable** (dropped 2026-10-13,
|
||
from the former *preflight*). The panel can only dial its own public
|
||
address from inside the container: behind NAT without hairpin that shows an
|
||
open port as closed, and with hairpin it shows as open a port the provider
|
||
blocks from outside. A card that is wrong both ways is worse than none, and
|
||
an honest check needs a probe outside the server, which is outside the
|
||
product ([product.md](product.md)). The administrator's browser cannot do
|
||
it either: 25, 465 and 587 are on the Fetch standard's list of blocked
|
||
ports. Two substitutes were offered and declined the same day: a Health
|
||
line "last connection from outside on each port", read from `mail.log`, and
|
||
a command in the help to run from one's own machine. **Returns if:** the
|
||
owner accepts an outside probe, or asks for one of the two substitutes.
|
||
- **preflight as a page of its own** (dropped 2026-10-13): its installation
|
||
checks went to Server › Health (health-setup-checks, `2.11.0`) and its test
|
||
e-mail to a form of its own (test-email, `2.12.0`). Its plan,
|
||
`plans/preflight.md`, was removed and stays in git history.
|
||
- **csrf-tokens** (decided 2026-10-13) - session-bound tokens on every form.
|
||
Replaced by origin-strict (`2.10.4`); the conditions for building
|
||
tokens after all are in [security.md](security.md), ADR § *CSRF via origin
|
||
checking*.
|
||
|
||
---
|
||
|
||
## Ideas, not on the roadmap
|
||
|
||
Thoughts kept so they are not lost. They are not candidates: nothing here is
|
||
waiting for an OK, has a version or takes a place in the order above. An idea
|
||
joins the index only when the owner says so.
|
||
|
||
- **Inbound quarantine** (moved here by the owner on 2026-10-12: "an idea to
|
||
think about") - hold suspicious inbound mail, review it in the panel, then
|
||
release it to the upstream or discard it. It would take the relay from
|
||
passing mail on to storing it for a while, against "no mailboxes" in
|
||
[product.md](product.md). Notes and open questions:
|
||
[plans/inbound-quarantine.md](plans/inbound-quarantine.md); the screens
|
||
`in-quarantine` and `in-quarantine-message` are drawn and stay unbuilt.
|