diff --git a/CHANGELOG.md b/CHANGELOG.md index bb7a428..4dd4792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,10 +44,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version rspamd rereads a changed file by itself. Deny wins over allow. The files are there from the first start with the inbound relay on. - **Inbound › Log**: the journal as a page - time, sender and client address, - recipient, subject, decision and why, filtered by domain, decision and day. - A user with the *domain* role sees the rows of the inbound domains assigned - to them. For the global role each row links to the filter lists with the - sender filled in (*Deny sender* / *Allow sender*). + recipient, subject and decision, filtered by domain, decision and day. The + decision opens a sheet over the page with who decided and why: the filter's + verdict and symbols, or the reply a refusal was given. A user with the + *domain* role sees the rows of the inbound domains assigned to them. For the + global role a row links to the filter lists with the sender filled in: + *Deny sender* on accepted mail, *Allow sender* on mail the filter refused. - **Inbound › Filter lists** (global role): add and remove allow and deny entries; the page says where the lists go and whether the filter is on to read them. @@ -55,6 +57,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version relay on. - Help: *Inbound log* and *Filter lists*. - New setting `ANTISPAM_DIR` (default `/data/antispam`). +- A full backup carries the inbound log, the filter lists and their files + under `data/antispam/`. It does not carry the `antispam/` directory with the + compose fragment and the rspamd configuration - copy that from + `deploy/antispam/` when restoring. - New setting `INBOUND_JOURNAL_MILTER_SOCKET` (default `/run/selfpost/inbound-journal.sock`). diff --git a/docs/guide.md b/docs/guide.md index 833f253..b8e8815 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -712,6 +712,15 @@ needed) for the new hostname or proxy, then `docker compose up -d`. The archive carries `certs/` from the old host — re-issue certificates when the hostname or IP changes. Set up the reverse-proxy vhost separately (not in the backup). +The inbound log and the filter lists are in the database and so in the +backup, and so are the list files under `data/antispam/`; the panel writes +those files again from the database on the first start after a restore. What +the backup does not hold is the spam filter's own side: the `antispam/` +directory with the compose fragment and the rspamd configuration. Like the +reverse-proxy fragments it is part of the installation, not of its state - +copy it from [deploy/antispam/](../deploy/antispam/) again, or the restored +instance runs with no filter. + **Restoring an encrypted (`.spbk`) backup** needs a running container to decrypt it first — any container with the `selfpost-backup` CLI works; decryption does not read `/data` and performs no version check. Start one normally diff --git a/docs/plans/inbound-antispam-panel.md b/docs/plans/inbound-antispam-panel.md index ac5d5f2..0a36ef2 100644 --- a/docs/plans/inbound-antispam-panel.md +++ b/docs/plans/inbound-antispam-panel.md @@ -91,6 +91,15 @@ them and nothing else, without the *Allow / Deny sender* links. That is what the accepted `in-log` mockup draws (no "global role" in its head). The owner was told and can turn it back into a global-only page. +**Inbound › Log accepted by the owner on 2026-10-12** ("Этот экран +утверждаем"), from screenshots of the page built from its fixture, after his +changes to it: the sender column is headed *From*; *Why* is not a column but +a sheet over the page, opened by the decision tag, holding a centred table of +who decided, the verdict and symbols or the reply, and the subject whole +(kept on the sheet at his word: the table clips it); a message Postfix itself +refused offers no *Deny sender*. Inbound › Filter lists is built and not yet +accepted. + ### Retention Mirror send-log retention: @@ -256,7 +265,7 @@ Target version cut: **`2.1.0`** (MINOR), after `2.0.0`. One commit per step; - [x] mail.log tailer: inbound reject rows — **Composer** - [x] List CRUD + validation + atomic rspamd map sync — **Composer** - [x] Panel: Inbound › Log and Inbound › Filter lists from the kit partials, mockups `in-log` / `in-filter-lists` (add client IP and engine columns mockup-first), evidence pairs — **Composer** -- [ ] Backup includes `/data/antispam/` — **Composer** +- [x] Backup includes `/data/antispam/` — **Composer** (it always did: a backup walks all of `/data`; now held by a test, and the guide says the `antispam/` fragment directory is not in it) - [ ] Unit + handler tests — **Composer** - [ ] [guide.md](../guide.md), [architecture.md](../architecture.md) — **Composer** - [ ] `go vet`, `go test`, e2e if applicable — **Composer** diff --git a/docs/roadmap.md b/docs/roadmap.md index 2bfd14f..073b7ed 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -30,7 +30,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md). |---|---|---|---|---| | preflight | Installation check page (`/preflight`) | candidate | — | [plans/preflight.md](plans/preflight.md) | | contributing | `CONTRIBUTING.md` | candidate | — | — | -| inbound-antispam-panel | Inbound antispam journal + allow/deny lists | in progress | 6/12 | [plans/inbound-antispam-panel.md](plans/inbound-antispam-panel.md) | +| inbound-antispam-panel | Inbound antispam journal + allow/deny lists | in progress | 7/12 | [plans/inbound-antispam-panel.md](plans/inbound-antispam-panel.md) | | inbound-quarantine | Inbound spam quarantine (hold / review / release) | candidate | — | [plans/inbound-quarantine.md](plans/inbound-quarantine.md) | | csrf-tokens | Per-session CSRF tokens on state-changing forms | candidate | — | — | | template-data-typing | Typed structs for template data instead of `map[string]any` | candidate | — | — | diff --git a/internal/backup/backup_test.go b/internal/backup/backup_test.go index 9edd6ed..4c580a3 100644 --- a/internal/backup/backup_test.go +++ b/internal/backup/backup_test.go @@ -42,6 +42,7 @@ func seedDataDir(t *testing.T) (dataDir, dbPath, deployRoot string) { writeFile(t, filepath.Join(dataDir, "opendkim", "keys", "example.com", "selfpost.private"), "PRIVATE KEY") writeFile(t, filepath.Join(dataDir, "sasl", "sasldb2"), "SASLDB") writeFile(t, filepath.Join(dataDir, "postfix", "sender_login_maps"), "@example.com login") + writeFile(t, filepath.Join(dataDir, "antispam", "deny_domain.map"), "evil.example\n") writeFile(t, filepath.Join(dataDir, "postfix", "queue", "deferred", "sample"), "queue-file") // Transient files that must NOT be archived. writeFile(t, filepath.Join(dataDir, "setup-token"), "secret-token") @@ -114,6 +115,10 @@ func TestCreateIncludesStateExcludesTransient(t *testing.T) { DataArchivePrefix + "opendkim/keys/example.com/selfpost.private", DataArchivePrefix + "sasl/sasldb2", DataArchivePrefix + "postfix/sender_login_maps", + // The filter lists as the spam filter reads them. They are rewritten + // from the database on the first start after a restore; carrying them + // means the filter has its lists before the panel is up. + DataArchivePrefix + "antispam/deny_domain.map", ComposeFileName, EnvFileName, CertsDirName + "/fullchain.pem",