release: 1.1.0: Domain-admin role and internal/web package split
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).
This commit is contained in:
+11
-3
@@ -188,10 +188,10 @@ holds the cookie works after process restart, redeploy, or full backup restore.
|
||||
- **Idle timeout** — sliding window, `PANEL_SESSION_IDLE_DAYS` (default 7); no
|
||||
absolute cap (regular use keeps the session alive indefinitely).
|
||||
- **Renewal** — DB `last_seen` and cookie `Max-Age` update at most once per hour
|
||||
(`renewThreshold` in [internal/web/session.go](../internal/web/session.go)).
|
||||
(`renewThreshold` in [internal/web/auth/session.go](../internal/web/auth/session.go)).
|
||||
- **Password change** — all other sessions are deleted; the current session stays
|
||||
active ([internal/store/sessions.go](../internal/store/sessions.go),
|
||||
[handlers_account.go](../internal/web/handlers_account.go)).
|
||||
[handlers_account.go](../internal/web/handlers/handlers_account.go)).
|
||||
|
||||
Restoring an **older** backup also restores session rows: a session invalidated
|
||||
after that backup was taken can become valid again if the browser still has the
|
||||
@@ -218,7 +218,15 @@ flowchart TB
|
||||
backupcli["selfpost-backup CLI"]
|
||||
end
|
||||
subgraph web ["internal/web — HTTP surface"]
|
||||
handlers["handlers_*.go, templates, session/security"]
|
||||
webRoot["web.go — router, security"]
|
||||
viewPkg["web/view — templates, static"]
|
||||
authPkg["web/auth — session, login, setup"]
|
||||
handlersPkg["web/handlers — authenticated pages"]
|
||||
webRoot --> viewPkg
|
||||
webRoot --> authPkg
|
||||
webRoot --> handlersPkg
|
||||
handlersPkg --> authPkg
|
||||
handlersPkg --> viewPkg
|
||||
end
|
||||
subgraph services ["Services — multi-store operations + rollback"]
|
||||
domainSvc["internal/domain"]
|
||||
|
||||
+14
-4
@@ -92,8 +92,9 @@ mechanics → Haiku. Reviewers must not be the author of the code under review.
|
||||
|
||||
## External libraries
|
||||
|
||||
The project is **AGPL-3.0** ([LICENSE](../LICENSE)). New Go dependencies must
|
||||
be permissive or GPL-family (see
|
||||
The project is **AGPL-3.0** ([LICENSE](../LICENSE)). Copyright holder and
|
||||
third-party notices: [NOTICE](../NOTICE). New Go dependencies must be
|
||||
permissive or GPL-family (see
|
||||
[.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)).
|
||||
|
||||
### Main module (`go.mod`)
|
||||
@@ -107,6 +108,12 @@ be permissive or GPL-family (see
|
||||
Transitive dependencies — `go mod graph` / `go.sum`; all indirect packages in
|
||||
the tree are AGPL-3.0-compatible.
|
||||
|
||||
### Vendored front-end
|
||||
|
||||
| Asset | Version | Repository | License |
|
||||
|---|---|---|---|
|
||||
| `internal/web/view/static/htmx.min.js` | 2.0.4 | <https://github.com/bigskysoftware/htmx> | 0BSD |
|
||||
|
||||
### E2e module (`test/e2e/go.mod`)
|
||||
|
||||
| Package | Version | Repository | License |
|
||||
@@ -121,6 +128,8 @@ the image.
|
||||
Postfix, OpenDKIM, `supervisord`, `sasl2-bin`, `logrotate`, and others come
|
||||
from Debian bookworm repositories; licenses are in each package's `copyright`
|
||||
file on <https://packages.debian.org/bookworm/>.
|
||||
The image also ships [LICENSE](../LICENSE) and [NOTICE](../NOTICE) under
|
||||
`/usr/share/doc/selfpost/`. The panel serves the AGPL text at `/license`.
|
||||
|
||||
---
|
||||
|
||||
@@ -132,7 +141,7 @@ Requires Go 1.26+ and `CGO_ENABLED=0`.
|
||||
|
||||
```sh
|
||||
make build # bin/panel, bin/selfpost-backup (VERSION=dev by default)
|
||||
make build VERSION=1.0.1
|
||||
make build VERSION=1.1.0
|
||||
```
|
||||
|
||||
Or directly:
|
||||
@@ -316,6 +325,7 @@ There is no `docs/archive/` directory.
|
||||
| [guide.md](guide.md) | Proxy, env, DNS, IP warmup, operations, rate limiting, backup, ports, image tag |
|
||||
| [SECURITY.md](../SECURITY.md) | Private reporting channel, supported versions, scope |
|
||||
| [LICENSE](../LICENSE) | AGPL-3.0 full text |
|
||||
| [NOTICE](../NOTICE) | Copyright holder and third-party attributions |
|
||||
| [deploy/docker-compose.yml](../deploy/docker-compose.yml) + proxies | Apache + nginx/Caddy/Traefik under [deploy/](../deploy/) |
|
||||
| [deploy/.env.example](../deploy/.env.example) | Public env template; full reference in [guide.md](guide.md) |
|
||||
| [CHANGELOG.md](../CHANGELOG.md) | Keep a Changelog |
|
||||
@@ -344,7 +354,7 @@ to prose.
|
||||
| Mail path | [build/postfix-config.sh](../build/postfix-config.sh) |
|
||||
| Panel routes | [internal/web/web.go](../internal/web/web.go) |
|
||||
| Backup / restore, domain export | [internal/backup/](../internal/backup/), [cmd/selfpost-backup/](../cmd/selfpost-backup/) |
|
||||
| Sessions | [internal/store/sessions.go](../internal/store/sessions.go), [internal/web/session.go](../internal/web/session.go) |
|
||||
| Sessions | [internal/store/sessions.go](../internal/store/sessions.go), [internal/web/auth/session.go](../internal/web/auth/session.go) |
|
||||
| Log rotation, reload | [build/logrotate-mail.conf](../build/logrotate-mail.conf), [build/logrotate-loop.sh](../build/logrotate-loop.sh), [build/postfix-cert-reload.sh](../build/postfix-cert-reload.sh) |
|
||||
| Deploy | [deploy/docker-compose.yml](../deploy/docker-compose.yml), [build/Dockerfile](../build/Dockerfile) |
|
||||
| Operator checklist | [§ User-facing deliverables](#user-facing-deliverables); detail — [guide.md](guide.md) |
|
||||
|
||||
+10
-4
@@ -148,8 +148,13 @@ Two different scopes — don't confuse them:
|
||||
behalf (e.g. `v=spf1 a mx ip4:<server IP> -all`, adjusted to your setup).
|
||||
- **DKIM** — a TXT record with the exact value the panel shows on that
|
||||
domain's page (`domain page → DKIM TXT record`), one selector per domain.
|
||||
- **DMARC** — a `_dmarc` TXT record (even a conservative `p=none` starts
|
||||
building reporting/reputation history).
|
||||
- **DMARC** — a `_dmarc` TXT record. The panel suggests `p=none` (monitoring
|
||||
only, safe to publish immediately). On a send-only relay the sending domain
|
||||
often has no inbox, so `rua=` is optional — configure a default report address
|
||||
in *Settings* or per domain when you have a mailbox that receives inbound mail
|
||||
elsewhere. If `rua=` points at another domain, publish `_report._dmarc` on that
|
||||
hub domain too; the panel checks it. Public mail hosts (Gmail, Outlook, …)
|
||||
cannot be used as external report destinations.
|
||||
|
||||
Skipping any of the three per-domain records is the single most common reason
|
||||
mail lands in spam even though SelfPost delivered it correctly — DKIM passing
|
||||
@@ -160,7 +165,8 @@ The panel checks both scopes for you and tells you what is actually published:
|
||||
the *Status* page verifies the server's hostname and its reverse record
|
||||
(forward-confirmed reverse DNS), and each domain's page shows a *DNS status*
|
||||
card comparing the published DKIM record against the key this server signs with,
|
||||
plus the domain's SPF and DMARC records. Results are cached for a few minutes;
|
||||
plus the domain's SPF, DMARC, and (when configured) DMARC report-authorisation
|
||||
records. Results are cached for a few minutes;
|
||||
use *Re-check* right after publishing a record. The SPF check is deliberately
|
||||
shallow — it looks for a mechanism that literally covers this server's address
|
||||
and does not follow `include:` or `redirect=`, so a record that authorizes the
|
||||
@@ -375,7 +381,7 @@ but it can look like an open port in external scans.
|
||||
## Fixed image tag
|
||||
|
||||
`deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`),
|
||||
deliberately never `:latest`. The current pin is `1.0.1`. Intermediate
|
||||
deliberately never `:latest`. The current pin is `1.1.0`. Intermediate
|
||||
CHANGELOG sections (`0.2.0`…`0.6.0`) record development cuts from before that
|
||||
image was published. Pinning matters because of the backup version check above:
|
||||
the panel binary's embedded version and the image tag that produced it are the
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Plan: dmarc-reports
|
||||
|
||||
**Status:** candidate
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
SelfPost **receives** DMARC aggregate reports on SMTP, parses them inside the
|
||||
image, and **shows summaries in the panel** — pass/fail by source, hints when
|
||||
`tighten p=` is reasonable. No external DMARC SaaS and no IMAP workflow for the
|
||||
operator.
|
||||
|
||||
## Scope
|
||||
|
||||
**In:**
|
||||
- Inbound SMTP for configured report addresses only (not a general backup-MX).
|
||||
- gzip + XML aggregate parsing → SQLite summaries per sending domain.
|
||||
- Panel page and/or per-domain section: recent reports, third-party senders,
|
||||
delivery health of report ingestion.
|
||||
- Reuse `admin.dmarc_report_email` and `domains.dmarc_rua` for DNS templates;
|
||||
when enabled, suggest a SelfPost-hosted report address.
|
||||
|
||||
**Out:**
|
||||
- Forensic reports (`ruf=`).
|
||||
- Full dashboards, APIs, email alerting.
|
||||
- Mailboxes for people (IMAP/POP3/webmail).
|
||||
|
||||
## Architecture (sketch)
|
||||
|
||||
1. Receiving MTAs → SMTP to SelfPost (hub MX).
|
||||
2. Postfix virtual alias or dedicated listener → panel ingest worker.
|
||||
3. Parse XML → `dmarc_reports` table (domain, reporter, counts, date).
|
||||
4. Panel reads SQLite; links from domain DNS card.
|
||||
|
||||
May share port-25 plumbing with [inbound-relay.md](inbound-relay.md) but must
|
||||
remain a separate, opt-in feature that does not forward mail upstream.
|
||||
|
||||
## Done when
|
||||
|
||||
- Operator can point `rua=` at an address SelfPost accepts and see parsed
|
||||
summaries in the panel within one reporting cycle.
|
||||
- With the feature off, outbound-only behaviour is unchanged.
|
||||
- Documented in [guide.md](../guide.md); migrations are backward-compatible.
|
||||
|
||||
## Risks
|
||||
|
||||
- Attack surface of accepting mail (mitigate: strict recipient allow-list).
|
||||
- Report volume and retention (mitigate: caps + pruning).
|
||||
@@ -23,7 +23,7 @@ list:
|
||||
deletion, its own L2 limit);
|
||||
- the domain's DKIM/DNS status;
|
||||
- the send log filtered to the domain — the filter already exists in the log
|
||||
([sendLogData](../../internal/web/handlers_monitor.go)).
|
||||
([sendLogData](../../internal/web/handlers/handlers_monitor.go)).
|
||||
|
||||
What stays outside the role is what is global by nature:
|
||||
|
||||
|
||||
+18
-4
@@ -1,6 +1,6 @@
|
||||
# Plan: web-split (splitting `internal/web`)
|
||||
|
||||
**Status:** agreed
|
||||
**Status:** done (see [CHANGELOG](../CHANGELOG.md) `[1.1.0]`)
|
||||
**Version:** `1.x`; an internal refactor, it does not force a break on its own.
|
||||
|
||||
---
|
||||
@@ -42,11 +42,25 @@ domains. The refactor is cheaper before that growth than after it.
|
||||
|
||||
The order is a recommendation, not a blocker.
|
||||
|
||||
## Chosen scheme
|
||||
|
||||
**Horizontal split into four packages** (decided at implementation):
|
||||
|
||||
```
|
||||
internal/web/ # Config, Server, New, Handler — composition root; security.go
|
||||
internal/web/view/ # embed templates/static, render/renderFragment, staticHandler
|
||||
internal/web/auth/ # session, login/logout/setup, requireAuth, currentUser
|
||||
internal/web/validate/ # shared form validation (avoids auth ↔ handlers import cycle)
|
||||
internal/web/handlers/ # all authenticated page handlers (handlers_*.go)
|
||||
```
|
||||
|
||||
`cmd/panel` keeps importing only `internal/web`. Subpackages are not exported
|
||||
beyond what the composition root needs.
|
||||
|
||||
## Done when
|
||||
|
||||
The decision is made deliberately when the work starts — either the package is
|
||||
split along the chosen scheme, or it is settled that it stays flat. After a
|
||||
split: `build`/`vet`/`test` green, the panel's behaviour unchanged.
|
||||
The package is split along the scheme above. After the split: `build`/`vet`/`test`
|
||||
green, the panel's behaviour unchanged.
|
||||
|
||||
## Risks
|
||||
|
||||
|
||||
+56
-27
@@ -26,15 +26,15 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||||
|
||||
| ID | Topic | Status | Plan |
|
||||
|---|---|---|---|
|
||||
| web-split | Splitting `internal/web` | **agreed** | [plans/web-split.md](plans/web-split.md) |
|
||||
| domain-admin | Domain administrator role | **agreed** | [plans/domain-admin.md](plans/domain-admin.md) |
|
||||
| inbound-relay | Inbound relay (backup-MX / forwarding) | **agreed** | [plans/inbound-relay.md](plans/inbound-relay.md) |
|
||||
| contributing | `CONTRIBUTING.md` | candidate | — |
|
||||
| visual-style | Обновление визуального стиля | candidate | — |
|
||||
| dmarc-reports | DMARC aggregate report ingestion and panel UI | candidate | [plans/dmarc-reports.md](plans/dmarc-reports.md) |
|
||||
|
||||
**Recommended order** (not binding): **web-split → domain-admin →
|
||||
inbound-relay** — first the package split, then role-wide authorisation, then
|
||||
the new vertical slice of the inbound relay. Deviating is allowed; there are no
|
||||
hard phases here.
|
||||
**Recommended order** (not binding): **domain-admin →
|
||||
inbound-relay** — role-wide authorisation first, then the inbound relay vertical
|
||||
slice. Deviating is allowed; there are no hard phases here.
|
||||
|
||||
After a context reset, pick an item marked `agreed` or `in progress`, then work
|
||||
the checklist in its linked plan.
|
||||
@@ -57,8 +57,7 @@ engine stays outside the image, only the attachment point is provided.
|
||||
|
||||
**Dependencies / risks:** a finished outbound path; open relay and backscatter;
|
||||
a wider attack surface (port 25 accepting mail).
|
||||
**Order:** recommended after [web-split](plans/web-split.md) and
|
||||
[domain-admin](plans/domain-admin.md).
|
||||
**Order:** recommended after [domain-admin](plans/domain-admin.md).
|
||||
**Version:** target bump `1.x`; `2.x` possible — to be settled once the
|
||||
implementation lands.
|
||||
|
||||
@@ -78,32 +77,13 @@ assigned domains (one or several).
|
||||
**Done when:** see [plans/domain-admin.md](plans/domain-admin.md).
|
||||
|
||||
**Dependencies / risks:** a users table, the role in the session, authorisation
|
||||
in every handler, setup and backup. **Order:** recommended after
|
||||
[web-split](plans/web-split.md), before
|
||||
in every handler, setup and backup. **Order:** recommended **before**
|
||||
[inbound-relay](plans/inbound-relay.md).
|
||||
**Version:** `1.x` MINOR, given a compatible migration of the current
|
||||
administrator into a global one.
|
||||
|
||||
---
|
||||
|
||||
## web-split
|
||||
|
||||
**Goal:** deliberately split `internal/web` (or settle on keeping the package
|
||||
flat) before it grows under inbound-relay and domain-admin.
|
||||
|
||||
**Boundary:** an internal refactor; the panel's behaviour for the operator does
|
||||
not change.
|
||||
|
||||
**Done when:** the package is split along the chosen scheme, or it is settled
|
||||
that it stays flat — see [plans/web-split.md](plans/web-split.md).
|
||||
|
||||
**Dependencies / risks:** exporting a package-private API. **Order:**
|
||||
recommended **first** among the agreed features (before domain-admin and
|
||||
inbound-relay).
|
||||
**Version:** `1.x`; on its own it does not force a break.
|
||||
|
||||
---
|
||||
|
||||
## contributing
|
||||
|
||||
**Goal:** `CONTRIBUTING.md` in the root — the dev loop, the checks to run
|
||||
@@ -119,3 +99,52 @@ it.
|
||||
**Dependencies / risks:** with a single developer and no PRs, this is low
|
||||
priority.
|
||||
**Version:** no bearing on semver.
|
||||
|
||||
---
|
||||
|
||||
## visual-style
|
||||
|
||||
**Goal:** refresh the control panel's visual design — typography, colour tokens,
|
||||
spacing, and component styling — without changing operator workflows or panel
|
||||
behaviour.
|
||||
|
||||
**Boundary:** presentation only (`panel.css`, templates, static assets); no new
|
||||
features. Styling must stay compatible with the panel CSP — rules live in
|
||||
`panel.css`, not inline (see [security.md](security.md) and the stylesheet
|
||||
header).
|
||||
|
||||
**Done when:** the panel reflects an agreed visual direction (starting
|
||||
reference: [assets/selfpost-proof.html](assets/selfpost-proof.html)); light and
|
||||
dark schemes remain supported; readability and contrast are preserved.
|
||||
|
||||
**Dependencies / risks:** CSP constraints on how styles are applied; visual
|
||||
regression across pages; low priority relative to functional work — take up
|
||||
after explicit agreement, independently of the feature roadmap order.
|
||||
**Version:** no bearing on semver.
|
||||
|
||||
---
|
||||
|
||||
## dmarc-reports
|
||||
|
||||
**Goal:** SelfPost receives DMARC aggregate reports (RFC 7489) on SMTP,
|
||||
parses the gzip/XML payloads, and shows pass/fail summaries in the panel — so
|
||||
the operator does not need an external DMARC service or a separate mailbox
|
||||
workflow.
|
||||
|
||||
**Boundary:** an extension of v1.0 — not IMAP/webmail and not a general
|
||||
inbound relay. A dedicated inbound path for report messages only; forensic
|
||||
reports (`ruf=`) out of scope for v1.
|
||||
|
||||
**Done when:** see [plans/dmarc-reports.md](plans/dmarc-reports.md).
|
||||
|
||||
**Dependencies / risks:** inbound SMTP in the image (may share infrastructure
|
||||
with [inbound-relay](plans/inbound-relay.md) but must not require backup-MX);
|
||||
storage and retention of parsed summaries; the `admin.dmarc_report_email` and
|
||||
`domains.dmarc_rua` settings added in the DMARC template work must stay the
|
||||
source of truth for `rua=` in DNS guidance.
|
||||
|
||||
**Order:** after the DMARC `rua=` settings ship; may follow or overlap with
|
||||
inbound-relay depending on how port 25 acceptance is structured.
|
||||
|
||||
**Version:** `1.x` MINOR.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user