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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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).
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).
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).
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).
Optional inbound relay (backup-MX / forwarder) on port 25, off by default. Mail is accepted only for domains configured in the panel and forwarded to an upstream; there are still no mailboxes. A dedicated security review of the inbound path is still open on the plan (Fable).
Added:
- `INBOUND_RELAY_ENABLE` (default `false`): when true, Postfix listens on
- Panel **Inbound** pages (global administrator): domains, upstream host/port
- Optional `INBOUND_ANTISPAM_MILTER` on the inbound listener only, plus
- Inbound rate limit (`INBOUND_RATE_LIMIT_MESSAGES_PER_IP`, default 20) and
Changed:
- Compose publishes host port 25 even when the flag is off (nothing listens
- Full-backup restore Resync also rebuilds inbound maps when the flag is on.
Full detail: CHANGELOG.md (1.4.0).
[1.3.0]
Security and quality after 1.2.5: domain-admin send-log authorization, fail-closed sign-in and application delete, level-2 rate-limit race fix, restore Resync, expanded tests, operator docs, release CI, and OFL for IBM Plex. Upgrading from 1.2.x is a tag bump; no migration.
Added:
- licence: the SIL Open Font License 1.1 text now travels with the IBM Plex
- docs: agreed roadmap item **queue-retries** — show this Postfix's retry
Security:
- The independent security review of the send-log authorization and
Fixed:
- test (e2e): send-log status scrapers follow the badge markup in
- security (panel): the Deliveries list is scoped to a domain administrator's
- mail (level-2 rate limit): the ceiling is no longer overshot by messages that
- panel (sign-in): a session that cannot be written to the database no longer
- panel (applications): deleting an application removes its SASL credentials
… (2 more in CHANGELOG)
Changed:
- docs: operator and as-built docs aligned with the code after a full
- docs: [guide.md](docs/guide.md) reorganised into **Installation**, **Instance
- docs: the 2026-08-13 full-tree review plan is complete — every phase (P0–P7)
- licence: [NOTICE](NOTICE) tells modifiers to update `SourceURL` in
- ci: the release image is published only for a **published** GitHub Release
… (9 more in CHANGELOG)
[1.3.1]
Retry policy in the panel, persistent Postfix queue, and self-contained full backups. Mail queue and delivery history show this Postfix's first retry, backoff cap, and queue lifetime. Deferred mail survives container recreate. Full backups now archive `data/`, compose, `.env`, and `certs/`; the archive layout breaks 1.3.0 flat backups (unpack those with `tar xzf backup.tar.gz -C ./data` as before). Upgrading from 1.3.0 is a tag bump; no schema migration.
Added:
- Mail queue and a delivery's history show this Postfix's retry policy
- docs: **Plan checklists** in [development.md](docs/development.md) — format,
- docs: **Implementation checklists** with model routing in
- docs: roadmap candidates **send-log-retention** (panel Settings for delivery
- Postfix queue under `/data/postfix/queue` — deferred and active mail survive
… (1 more in CHANGELOG)
Changed:
- **Breaking:** full-backup archive layout — paths are prefixed with `data/`;
- docs: operator guide, architecture, security, and backup UI updated for
- docs: HTML mockups for a full panel UI refresh
Changed:
- docs: panel UI mockups after review — Status restored to live readings
- docs: panel UI design system and mockups rebuilt as separate pages
- docs: panel UI mockups — add-domain sits in the list card (Domains and
Fixed:
- ci (GHCR): per-arch package tags (`X.Y.Z-amd64`, `X.Y.Z-arm64`) are dropped
Squashed CHANGELOG sections: [1.3.0], [1.3.1].
Full detail: CHANGELOG.md (1.3.0, 1.3.1).
[1.2.0]
The second MINOR after 1.0.0: domain administrators with per-domain scope, a panel visual refresh on the SelfPost palette, and refinements to navigation and the send log. Upgrading runs one SQLite migration (`0005_panel_users`); the single administrator becomes a global user.
Added:
- panel: **domain-admin role** — global administrators manage panel users and
Fixed:
- Docker build: `LICENSE` is no longer excluded by `.dockerignore`, so the
- panel: on signed-in pages with a narrow card (**Settings**, the user form)
Changed:
- panel: navigation session icons are distinct — **Settings** uses a gear,
- panel: `/` redirects domain administrators to `/domains`; global users still
- panel: **Sign out** in the navigation column uses the same type size and
- panel: **visual style** brought in line with the SelfPost mark — brick accent
- panel: the send log's **status is a badge**, in the same ok/warn/error/unknown
… (1 more in CHANGELOG)
[1.2.1]
Panel refinements after 1.2.0: navigation icons, status-badge centreing, drill-down back-link placement, and safeguards for the sole global administrator. Upgrading is a tag bump; no migration.
Fixed:
- panel: status badge text sat low in the box (and below the heading or label
- panel: the **Users** navigation icon was two full silhouettes with staggered
- panel: the **Settings** navigation icon was a sun-with-rays (circle plus
- panel: the user create/edit form placed «Back to users» at the bottom of the
Changed:
- panel: the user edit form disables role change and delete for the only global
- panel: the user create/edit form hides **Assigned domains** when the role is
- panel: **Settings** shows panel credentials and DMARC aggregate reports side
[1.2.2]
Status page layout after 1.2.1: paired cards in a wide column, denser machine and check copy, and a user-form checkbox fix. Upgrading is a tag bump; no migration.
Changed:
- panel: **Status** is wide again so paired cards fill the column. Layout:
Fixed:
- panel: cards inside `.split` used `margin: 0 auto`, which in a CSS grid
- panel: on the user create/edit form, **Assigned domains** checkboxes stacked
[1.2.3]
Domain detail layout and panel polish after 1.2.2. Upgrading is a tag bump; no migration.
Changed:
- panel: the domain detail page is wide with paired cards (DKIM+SPF ‖ DMARC;
- panel: Domains list — **Add domain** sits beside the domain field; the
- panel: page URLs, browser titles, and headings are aligned — **Settings** is
Fixed:
- image: `mail.log` rotation no longer silently stops when the build context
[1.2.4]
Level-2 rate-limit semantics inverted after 1.2.3, plus a small DNS field height fix. Upgrading is a tag bump; no migration.
Changed:
- rate limiting (level 2): domain ceilings apply to every client IP (no IP
Fixed:
- panel: on the domain DNS status grid, the Type (TXT) field height matches
[1.2.5]
Rate-limit form polish after 1.2.4. Upgrading is a tag bump; no migration.
Changed:
- panel: rate-limit forms refined — level-1 backstop as muted copy on the
Squashed CHANGELOG sections: [1.2.0], [1.2.1], [1.2.2], [1.2.3], [1.2.4], [1.2.5].
Full detail: CHANGELOG.md (1.2.0, 1.2.1, 1.2.2, 1.2.3, 1.2.4, 1.2.5).
The first MINOR after 1.0.0: send-only DMARC guidance in the panel, AGPL packaging on every page, and an internal split of `internal/web` ahead of domain-admin work. Upgrading runs one SQLite migration (empty defaults; existing DNS guidance is unchanged until you set a report address).
Changed:
- `internal/web` split into subpackages (`web/view`, `web/auth`, `web/validate`,
Added:
- panel: DMARC guidance for send-only relays — the suggested `_dmarc` record
- AGPL packaging hygiene: [NOTICE](NOTICE) names the copyright holder and the
- [docs/roadmap.md](docs/roadmap.md) — candidate item **visual-style** (panel
Full detail: CHANGELOG.md (1.1.0).
[1.0.0]
Added:
- A **Delivery log** on each delivery's page (`/deliveries/{id}`): the
- A **History** block on the same page: the journal's two timestamps stated as
Fixed:
- Postfix config: copy TLS cert/key from the (often `:ro`, host-owned) mount
- Postfix config: allow `maillog_file` under `/data` via
- E2e: read `/data/setup-token` via `docker compose exec` (file is `0600`
- Entrypoint: check `SELFPOST_HOSTNAME` before `/data` setup (so a bad identity
- E2e gate: wait for host-published `/healthz` before panel setup, and stop
… (3 more in CHANGELOG)
Changed:
- Document what `.spbk` and `.spde` stand for (SelfPost backup / SelfPost domain
- Docs aligned with the code: setup URL is `/setup/<token>` (README and
- Full backups no longer carry `/data/log`. It is Postfix's raw log plus its
- Monitoring screens (status, mail queue, system log, deliveries) use adaptive
- Documentation package consolidated into `docs/development.md`: Documentation
… (4 more in CHANGELOG)
[1.0.1]
A documentation and packaging release: no change to the mail path, the database, or the on-disk layout. Upgrading is a tag bump.
Added:
- `SECURITY.md` — how to report a vulnerability privately (GitHub private
- [docs/plans/](docs/plans/) — one document per agreed extension: the optional
Changed:
- The panel's **Account** entry is now called **Settings** — nav link, page
- The signed-in name in the panel's nav is now labelled `User:`, so it reads as
- [docs/product.md](docs/product.md) reframes the future line: the inbound
- [docs/security.md](docs/security.md) is now in English, matching the rest of
- The README documentation table now points at `SECURITY.md` for reporting a
… (2 more in CHANGELOG)
Fixed:
- The panel's static assets are served with a content ETag and
Squashed CHANGELOG sections: [1.0.0], [1.0.1].
Full detail: CHANGELOG.md (1.0.0, 1.0.1).
A page per delivery (`/deliveries/{id}`), reached from the *Details* link on
Added:
- A page per delivery (`/deliveries/{id}`), reached from the *Details* link on
- A **DNS** badge in the domain list, one per row, carrying the same
- Machine metrics on the status page: a **Machine** card reporting the
- An index of the current page's own sections in the navigation column, for the
Changed:
- Every panel page is laid out in one column of the same width, so moving
- The mark's small-size variant — the tab icon and the `SP` initials it
- The stamp's `SELF-HOSTED SMTP RELAY` line is set at 11.5/0.15 instead of
- The delivery log lists what identifies a message and nothing else: time,
- Subjects are decoded for display as well as on the way in, so the rows the
… (4 more in CHANGELOG)
Full detail: CHANGELOG.md (0.6.0).
A bounce could be recorded as a successful delivery. The log-tailer's
Fixed:
- A bounce could be recorded as a successful delivery. The log-tailer's
- Log-tailer resumes where it stopped instead of jumping to end-of-file on
- Level-2 rate limit no longer overshoots under concurrency: messages that
Changed:
- The project has a single public home: `github.com/mixeme/selfpost`. Codeberg
- Code comments no longer cite the archived specification. References like
- `docs/code-review.md` is gone. Its plan is finished — phases 0 (bar the
- `docs/architecture.md` gained a *Code layers* section: a diagram of
- Phase 1 doc/code hygiene (`docs/code-review.md`): removed ~30 stale
… (1 more in CHANGELOG)
Security:
- Pre-release security review (plan § D, model Fable, 2026-08-06): full pass
- `saslpasswd2` argv: the application login is now passed after a `--`
Added:
- Optional password encryption for the two secret-bearing downloads (plan
- Domain import accepts an encrypted export: the envelope is detected by its
- `selfpost-backup` writes encrypted archives and reads them back:
- `TestParseDelivery` covers the exotic mail.log shapes the review asked for
- docs: README *Encrypting a backup or export*; `docs/security.md` §
… (14 more in CHANGELOG)
Removed:
- docs: `security.md` — accepted risk «restore old backup revives session rows»
Changed:
- docs: `implementation-plan.md` trimmed to the sole open v1.x gate — pre-release
- docs: `architecture.md` — sessions (SQLite, idle renew, password change),
- docs: `development.md` — expanded e2e stack and `release.yml` matrix workflow.
- docs: `security.md` — accepted risk for send-log rows stuck at `queued` after
- docs: `roadmap.md` — optional send-log / `mail.log` follow-ups under v1.x tail.
… (10 more in CHANGELOG)
Full detail: CHANGELOG.md (0.5.0).
The project's mark is now in use rather than only on file. The README opens
Added:
- The project's mark is now in use rather than only on file. The README opens
Changed:
- panel: the sign-in and setup pages are now a column the width of their own
- panel: the three monitoring pages — Deliveries, Mail queue, System log — are
Fixed:
- panel: Deliveries now shows the subject as text rather than as its MIME
- panel: table cells may now break inside a word, so no single long value can
- panel: the Applications list on a domain page no longer comes apart. It was a
Full detail: CHANGELOG.md (0.4.0).
Initial feature-complete implementation of the v1.0 specification (phases 0-11 of `docs/implementation-plan.md`).
Added:
- Panel (Go, single static binary) with SQLite persistence, one-time
- Domain management with per-domain DKIM (RSA-2048, generated in pure Go) and
- Application (sender identity) management: SASL credentials via `sasldb2`,
- Full Postfix relay config generated from env at container start: SMTPS 465,
- Journal milter (pure Go, `go-milter`) recording every send to `send_log`;
… (6 more in CHANGELOG)
Full detail: CHANGELOG.md (0.1.0).